内容提要
本文解析 React v16.11 中 useEffect 的源码。useEffect 用于替代类组件生命周期中散乱的副作用逻辑,让开发者聚焦业务逻辑。它接收回调函数和依赖数组:无依赖时每次渲染执行,空数组仅首次执行。源码中首次挂载调用 mountEffect,更新时调用 updateEffect,通过 pushEffect 将 effect 存入循环链表,最终在 commit 阶段由 commitHookEffectList 执行 create 或 destroy。
延伸解读
从生命周期到副作用:useEffect 的设计动机
文章指出,类组件常把请求、事件绑定等逻辑分散在 componentDidMount 和 componentDidUpdate 中,导致业务逻辑散乱。useEffect 将关注点从“在哪个生命周期做什么”拉回“业务逻辑本身”,把副作用及其清理函数放在一起。这种组织方式让相关代码更内聚,也减少了在多个生命周期之间来回对照的负担。
依赖数组如何决定执行时机
useEffect 的第二个参数依赖数组直接影响回调执行频率:不传依赖时每次渲染后都执行;传空数组则仅首次渲染后执行;传入具体依赖时,只有依赖变化才执行。源码中 updateEffectImpl 通过 areHookInputsEqual 比较新旧依赖,相等时以 NoHookEffect 标记跳过本次执行,这与文章给出的简单实现逻辑一致。
Effect 链表与 commit 阶段的执行
每次调用 useEffect 都会通过 pushEffect 生成一个 effect 节点,并存入 componentUpdateQueue 的循环链表。渲染阶段这些 effect 被挂到 Fiber 的 updateQueue 上,直到 commit 阶段才由 commitHookEffectList 遍历执行:先根据 tag 判断是否执行 destroy(清理函数),再执行 create(副作用回调)。因此 useEffect 的回调并非在渲染过程中同步运行。
源码解析的边界与遗留问题
文章明确将 effect.tag 中除 NoHookEffect 外的二进制位含义、以及 commitBeforeMutationEffects、commitMutationEffects、commitLayoutEffects 三个函数的具体逻辑留到下一篇分析 useLayoutEffect 时展开。读者需注意,本文只完整覆盖了 useEffect 从挂载、更新到 commitHookEffectList 执行的主线,并未解释所有 tag 组合的行为。
Q&A
useEffect 和类组件的生命周期相比,主要解决了什么问题?
useEffect 主要解决类组件中业务逻辑散落在各个生命周期函数里的问题。比如事件绑定在 componentDidMount,解绑在 componentWillUnmount,逻辑不集中。useEffect 让开发者从“面向生命周期编程”转变为聚焦业务逻辑本身,把副作用逻辑(如订阅、事件绑定)组织在一起。
useEffect 的第二个参数(依赖数组)不同写法分别代表什么执行时机?
如果 dependencies 不存在,function 每次 render 都会执行;如果 dependencies 存在,只有当它发生变化时 function 才会执行;如果 dependencies 是空数组,那么当且仅当首次 render 的时候才会执行 function。
React 源码中 useEffect 在首次挂载和更新时分别调用哪个函数?
首次加载时 useEffect 实际执行的是 mountEffect,之后每次渲染执行的是 updateEffect。它们分别调用 mountEffectImpl 和 updateEffectImpl,传入相同的四个参数:fiberEffectTag、hookEffectTag、create 和 deps。
updateEffectImpl 中如何判断是否需要重新执行 effect?
updateEffectImpl 会获取当前 Hook 和上一次的 effect。如果 nextDeps 不为 null,则通过 areHookInputsEqual 比较新旧 deps;如果相等,就调用 pushEffect(NoHookEffect, create, destroy, nextDeps) 并直接返回,表示不执行本次 effect。否则会设置 sideEffectTag 并正常 pushEffect。
pushEffect 函数是如何存储 effect 的?
pushEffect 会创建一个 effect 对象(包含 tag、create、destroy、deps、next),并将其存入 componentUpdateQueue 的循环链表中。如果队列为空,则初始化并让 lastEffect 指向自身;如果队列不为空,则根据 lastEffect 是否为空进行插入,最终形成循环链表。
useEffect 的 create 和 destroy 函数最终在哪个阶段被执行?
在 commit 阶段,commitRootImpl 会遍历 effect 链表,依次执行 commitBeforeMutationEffects、commitMutationEffects 和 commitLayoutEffects,这些函数最终都会调用 commitHookEffectList。在 commitHookEffectList 中,根据 effect.tag 判断:如果匹配 unmountTag 则执行 destroy,如果匹配 mountTag 则执行 create 并将其返回值赋给 destroy。