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])
posted @ 2026-05-04 10:38  秀秀不只会前端  阅读(28)  评论(0)    收藏  举报