跳到主要内容

第 14 章:useEffect / useLayoutEffect——effect 链表与执行时机

useEffectuseLayoutEffect 的区别,面试常考:一个异步、一个同步。本章看源码,把"为什么"落到机制上。

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 也串一份(pushEffectImpl2580)。后者是一个环链表lastEffect 指向最新一个),commit 阶段就是沿着它把所有 effect 捞出来跑。

deps 比较:变没变,决定执行不执行

updateEffectImpl2632)的关键在 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 相等就不执行,这就是依赖数组的作用。注意传 [] 不是"没有依赖",而是"一个都不比,永远相等",所以只在首次跑。而首次挂载(mountEffectImpl2615)无条件打 HookHasEffect,保证第一次一定执行。

时机:Layout 同步,Passive 异步

useLayoutEffect 的注册(2771)和 useEffect2672)几乎一模一样,唯一的差别是传进去的 flag 不同

  • useLayoutEffectfiber.flags |= Layout,对应 commit 的 layout 阶段(第 17 章)。layout 阶段是 commit 的同步路径,在浏览器画帧之前跑完。这就是"同步、画面前"。
  • useEffectfiber.flags |= Passive,对应的执行在 commit 之后,被调度成异步任务(commitPassiveMountEffects,第 4 章地图里那个"passive effect 异步跑")。这就是"异步、画面后"。

这些 flag 顺着第 8 章的 bubbleProperties 一路冒泡到根,commit 阶段按 subtreeFlags 找到所有该跑的 effect。一套登记机制,两个 flag 分支,两种时机,就这回事。

动手实验

  1. 看 deps 跳过useEffect 依赖写 [count],在 areHookInputsEqual(ReactFiberHooks.js:454)打断点,count 没变的那次渲染,观察它返回 true、effect 没被标记执行。
  2. 对比执行顺序:一个组件里同时用 useEffectuseLayoutEffect 各打印一行,看控制台顺序。layout 那行在 paint 前,effect 那行在 paint 后。
  3. 扒 effect 环:在 pushEffectImpl(ReactFiberHooks.js:2580)打断点,看 fiber.updateQueue.lastEffect 的环链表怎么把多个 effect 串起来。

小结

  • useEffect/useLayoutEffect 都只是登记,执行在 commit 阶段。
  • effect 是环链表节点,同时挂在 hook.memoizedStatefiber.updateQueue 上。
  • deps 用 Object.is 逐个比,相等就跳过执行([] 等于永不变化)。
  • Layout flag 同步跑画面前,Passive flag 异步跑画面后,一套机制两种时机。
  • 下一步:把常用 hooks 逐个拆完,第 15 章。