第 14 章:useEffect / useLayoutEffect——effect 链表与执行时机
useEffect 和 useLayoutEffect 的区别,面试常考:一个异步、一个同步。本章看源码,把"为什么"落到机制上。
function App() {
useEffect(() => {
// 在浏览器画完这帧之后异步跑
}, [dep]);
useLayoutEffect(() => {
// 在浏览器画之前同步跑
}, [dep]);
return <div />;
}
先记住这一句:两个 hook 都只是登记一个 effect,真正执行在 commit 阶段。区别在给 effect 打的 flag:
useLayoutEffect打 Layout,同步跑在画面前;useEffect打 Passive,异步跑在画面后。deps 相等时连登记都变"白登记",直接跳过。
一图流:一个 effect 的登记与执行
effect 长什么样
effect 是个对象(ReactFiberHooks.js:221):
type Effect = {
tag: HookFlags, // 要不要执行(HookHasEffect)
inst: EffectInstance, // 存清理函数 destroy
create: () => (() => void) | void, // 你的回调
deps: Array<mixed> | void | null, // 依赖数组
next: Effect, // 指向下一个 effect
};
它同时挂两个地方:hook 的 memoizedState 存一份(第 12 章的链表节点),fiber 的 updateQueue 也串一份(pushEffectImpl,2580)。后者是一个环链表(lastEffect 指向最新一个),commit 阶段就是沿着它把所有 effect 捞出来跑。
deps 比较:变没变,决定执行不执行
updateEffectImpl(2632)的关键在 deps 比较(2650):
if (areHookInputsEqual(nextDeps, prevDeps)) {
hook.memoizedState = pushSimpleEffect(hookFlags, inst, create, nextDeps);
return; // deps 没变:登记 effect,但不打 HookHasEffect,执行阶段跳过它
}
currentlyRenderingFiber.flags |= fiberFlags; // deps 变了:给 fiber 打上要跑的标记
hook.memoizedState = pushSimpleEffect(HookHasEffect | hookFlags, ...);
areHookInputsEqual 就是一个逐个 Object.is 比较(454)。deps 相等就不执行,这就是依赖数组的作用。注意传 [] 不是"没有依赖",而是"一个都不比,永远相等",所以只在首次跑。而首次挂载(mountEffectImpl,2615)无条件打 HookHasEffect,保证第一次一定执行。
时机:Layout 同步,Passive 异步
useLayoutEffect 的注册(2771)和 useEffect(2672)几乎一模一样,唯一的差别是传进去的 flag 不同:
useLayoutEffect→fiber.flags |= Layout,对应 commit 的 layout 阶段(第 17 章)。layout 阶段是 commit 的同步路径,在浏览器画帧之前跑完。这就是"同步、画面前"。useEffect→fiber.flags |= Passive,对应的执行在 commit 之后,被调度成异步任务(commitPassiveMountEffects,第 4 章地图里那个"passive effect 异步跑")。这就是"异步、画面后"。
这些 flag 顺着第 8 章的 bubbleProperties 一路冒泡到根,commit 阶段按 subtreeFlags 找到所有该跑的 effect。一套登记机制,两个 flag 分支,两种时机,就这回事。
动手实验
- 看 deps 跳过:
useEffect依赖写[count],在areHookInputsEqual(ReactFiberHooks.js:454)打断点,count 没变的那次渲染,观察它返回 true、effect 没被标记执行。 - 对比执行顺序:一个组件里同时用
useEffect和useLayoutEffect各打印一行,看控制台顺序。layout 那行在 paint 前,effect 那行在 paint 后。 - 扒 effect 环:在
pushEffectImpl(ReactFiberHooks.js:2580)打断点,看fiber.updateQueue.lastEffect的环链表怎么把多个 effect 串起来。
小结
useEffect/useLayoutEffect都只是登记,执行在 commit 阶段。- effect 是环链表节点,同时挂在 hook.memoizedState 和 fiber.updateQueue 上。
- deps 用
Object.is逐个比,相等就跳过执行([]等于永不变化)。 - Layout flag 同步跑画面前,Passive flag 异步跑画面后,一套机制两种时机。
- 下一步:把常用 hooks 逐个拆完,第 15 章。