Appearance
7.3 useEffect: 副作用的实现原理
useEffect 是 React Hooks 中用于处理副作用(side effects)的核心。数据获取、DOM 操作、订阅等都属于副作用的范畴。useEffect 的设计精妙之处在于,它将副作用的执行与组件的渲染分离,推迟到浏览器完成绘制之后,从而避免了阻塞渲染。
与 useState 类似,useEffect 的实现在 Dispatcher 中也分为 mountEffect 和 updateEffect 两个版本。
1. Effect 对象的创建:pushEffect
无论是 mount 还是 update,useEffect 的核心都是调用 pushEffect 函数。这个函数负责创建一个 Effect 对象,并将其链接到 Fiber 的 updateQueue 上。这个 updateQueue 是一个专门为 effects 准备的环形链表。
Effect 对象的结构如下:
javascript
// In packages/react-reconciler/src/ReactFiberHooks.js
export type Effect = {
tag: HookFlags,
inst: EffectInstance, // 用于存储 destroy 函数
create: () => (() => void) | void, // 副作用函数
deps: Array<mixed> | void | null, // 依赖项数组
next: Effect, // 指向下一个 Effect
};pushEffect 函数将这个 Effect 对象添加到组件 Fiber 的 updateQueue.lastEffect 链表中,并根据 tag 在 Fiber 上设置相应的 flags(例如 PassiveEffect),以通知 commit 阶段需要处理这些副作用。
javascript
// In packages/react-reconciler/src/ReactFiberHooks.js
function pushEffect(
tag: HookFlags,
create: () => (() => void) | void,
inst: EffectInstance,
deps: Array<mixed> | void | null,
): Effect {
const effect: Effect = {
tag,
create,
inst,
deps,
// Circular
next: (null: any),
};
let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
if (componentUpdateQueue === null) {
// ... 创建 updateQueue
} else {
// 将 effect 插入到环形链表的末尾
const lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
}
return effect;
}2. 首次渲染:mountEffect
在组件首次渲染时,mountEffect 会被调用。它的逻辑相对简单:
- 创建一个新的 Hook,并将其添加到 Hooks 链表中。
- 调用
pushEffect,传入HookPassive | HookHasEffect作为tag,这会给当前 Fiber 添加PassiveEffect和UpdateEffect的 flags。 PassiveEffectflag 告诉 React 的 commit 阶段,在渲染完成后需要异步执行一个副作用。
javascript
// In packages/react-reconciler/src/ReactFiberHooks.js
function mountEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
// ... 省略 DEV 下的检查
return mountEffectImpl(
PassiveEffect | PassiveStaticEffect, // Fiber Flags
HookPassive, // Hook Flags
create,
deps,
);
}
function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// 在 Fiber 上打上副作用的标记
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
createEffectInstance(),
nextDeps,
);
}3. 更新渲染:updateEffect
在更新渲染时,updateEffect 的逻辑会复杂一些,因为它需要处理依赖项的变化:
- 获取当前 Hook 对象。
- 获取上一次的依赖项
prevDeps。 - 使用
areHookInputsEqual函数比较prevDeps和本次的nextDeps。 - 如果依赖项没有变化,则不执行任何操作。
- 如果依赖项发生变化,则调用
pushEffect创建一个新的Effect,并标记 Fiber 需要在 commit 阶段执行副作用。
javascript
// In packages/react-reconciler/src/ReactFiberHooks.js
function updateEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
// ...
return updateEffectImpl(PassiveEffect, HookPassive, create, deps);
}
function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
let destroy = undefined;
if (currentHook !== null) {
const prevEffect = currentHook.memoizedState;
destroy = prevEffect.inst;
if (nextDeps !== null) {
const prevDeps = prevEffect.deps;
// 比较依赖项
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 依赖项相同,标记为不需要执行
pushEffect(hookFlags, create, destroy, nextDeps);
return;
}
}
}
// 依赖项不同或首次渲染,标记 Fiber 需要执行副作用
currentlyRenderingFiber.flags |= fiberFlags;
// 标记 Effect 本身需要执行
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
destroy,
nextDeps,
);
}areHookInputsEqual 函数通过 Object.is 逐个比较依赖项数组中的值。这就是为什么 useEffect 的依赖项应该是值类型或者引用地址稳定的对象。
4. 副作用的执行与清理
在 render 阶段,useEffect 只是创建并标记了 Effect。真正的执行发生在 commit 阶段之后。
- 执行
create:在 commit 阶段的commitPassiveEffects函数中,React 会遍历updateQueue中的Effect链表,执行那些被标记为HookHasEffect的 effect 的create函数。create函数返回的清理函数会被保存在effect.inst.destroy上。 - 执行
destroy:在下一次依赖项变化导致 effect 重新执行之前,或者在组件卸载时,React 会先执行上一次保存在effect.inst.destroy中的清理函数。
通过这种方式,useEffect 巧妙地将副作用的声明(在 render 阶段)与执行(在 commit 阶段后)分离开来,确保了副作用不会阻塞 UI 渲染,并提供了一套完整的生命周期管理机制(创建、销毁、依赖更新)。