11_深入探索副作用 Hooks
一、总览
insertion → layout → effect(从“最早”到“最晚”)
二、核心数据结构
所有 effect 类 Hook,本质都挂在 Fiber 上。
1️⃣ Hook 节点
type Hook = {
memoizedState: Effect | null;
next: Hook | null;
}
2️⃣ Effect 结构(关键)
type Effect = {
tag: number; // Hook 类型(layout / passive / insertion)
create: () => void; // effect 回调
destroy: () => void;// cleanup 回调
deps: any[] | null;
next: Effect; // 与当前函数组件的其他 effect 形成环状链表
}
3️⃣ 存储位置
Fiber.updateQueue
↓
effect1 → effect2 → effect3(环形链表)
- Hook 链表(memoizedState):按调用顺序
- Effect 链表(updateQueue):按 effect 顺序
三、render 阶段:如何收集 effect?
入口:mountEffect / updateEffect
function mountEffect(create, deps) {
mountEffectImpl(PassiveEffect, HookPassive, create, deps);
}
核心逻辑
mount 阶段:
function mountEffectImpl(fiberFlags, hookFlags, create, deps) {
// 生成 hook 对象
const hook = mountWorkInProgressHook();
// 保存依赖的数组
const nextDeps = deps === undefined ? null : deps;
// 修改当前 fiber 的 flag
// 打标记 PassiveEffect / LayoutEffect / InsertionEffect ,用于 commit 阶段判断是否执行
currentlyRenderingFiber.flags |= fiberFlags;
// 将 pushEffect 返回的环形链表存储到 hook 对象的 memoizedState 中
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
undefined,
nextDeps
);
}
update 阶段:
function updateEffectImpl(
fiberFlags: Flags,
hookFlags: HookFlags,
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
// 拿到之前的 hook 对象
const hook = updateWorkInProgressHook();
// 拿到依赖项
const nextDeps = deps === undefined ? null : deps;
// 拿到副作用链表
const effect: Effect = hook.memoizedState;
const inst = effect.inst;
// 当渲染阶段状态 update 后或严格模式重新渲染时,currentHook 在初始 mount 时为 null。
if (currentHook !== null) {
// 新的依赖项不为空
if (nextDeps !== null) {
const prevEffect: Effect = currentHook.memoizedState;
const prevDeps = prevEffect.deps;
// 依赖项不变(浅比较)
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 给这个 effect 打上一个 HookPassive 的 tag,后续在组件渲染完以后会跳过这个 effect 的执行
hook.memoizedState = pushSimpleEffect(
hookFlags,
inst,
create,
nextDeps,
);
return;
}
}
}
// 依赖项改变, update 时,修改 flag,commit 时再次执行 effect
currentlyRenderingFiber.flags |= fiberFlags;
// pushEffect 将当前 effect 添加到 FiberNode 的 updateQueue 中,然后返回当前 effcet
// 再把返回的当前 effect 保存到 Hook 节点的 memoizedState 中
// 之后在 commit 阶段统一执行 effect,之后会推入新的 effect 到环状链表上
hook.memoizedState = pushSimpleEffect(
HookHasEffect | hookFlags,
inst,
create,
nextDeps,
);
}
四、useInsertionEffect
执行时机 beforeMutation 阶段 ,DOM 还没变
commitBeforeMutationEffects() {
// 执行 insertion effects
}
用途:CSS-in-JS:
useInsertionEffect(() => {
injectStyle();
}, []);
必须在这个阶段:如果样式在 DOM 更新后才插入 → 会闪烁(FOUC)
五、useLayoutEffect(同步布局)
执行时机,layout 阶段(DOM 已更新,浏览器未绘制)。
commitLayoutEffects() {
commitHookEffectListMount(LayoutEffect);
}
- 同步执行
- 阻塞浏览器绘制
典型用法:
useLayoutEffect(() => {
const rect = ref.current.getBoundingClientRect();
});
执行流程:
1. cleanup(旧)
2. create(新)
六、useEffect(异步副作用)
执行时机,commit 完成后(浏览器绘制后)
flushPassiveEffects()
执行流程:
1. cleanup(旧)
2. create(新)
- 不阻塞 UI
- 可延时
应用场景:
useEffect(() => {
fetchData();
}, []);
七、三者执行顺序(完整流程)
render(收集 effect)
↓
commit
↓
1️⃣ useInsertionEffect
↓
2️⃣ DOM mutation
↓
3️⃣ useLayoutEffect
↓
4️⃣ 浏览器绘制
↓
5️⃣ useEffect(异步)
八、并发模式下的 effect
render 可以被打断:
render → 中断 → 重新 render
effect 如何保证安全?
原则:
- 只有 commit 后才执行 effect
- render 阶段不会执行副作用
避免:重复请求 / 状态错乱
九、闭包陷阱
useEffect(() => {
console.log(count);
}, []);
输出旧值
原因 Hook 存的是:
create 函数(闭包已固定)
解决
useEffect(() => {}, [count])

浙公网安备 33010602011771号