banner
IWSR

IWSR

美少女爱好者,我永远喜欢芳泽瑾。另外 P5 天下第一!

React Hooks: useEffect与useLayoutEffect

TLNR#

我希望这篇文章可以帮助读者理解以下几点:

  1. use (layout) Effect 被调用时会创建 effct 对象,且该对象会以链表的形式被挂载到 fiber 的 updateQueue 上
  2. useEffect 由于会被 Scheduler 以 NormalSchedulerPriority 进行调度,因此它是一个异步操作
  3. useLayoutEffect 的 create 是在 layout 阶段被同步处理的,其上一轮的 destroy 是在 mutation 阶段被同步处理的,因此在该 hook 内调用耗时操作会阻塞页面更新

阅前须知#

  • useEffect 涉及到与 Scheduler 的交互,因此在理解其执行顺序时需要对 react 的调度行为有一定的了解。简单来说可以这样理解 (不严谨的说法会个意即可)
    • 所有通过 scheduleCallback 注册进 Scheduler 的任务都是一个异步任务,且该任务类型为 Task
    • react 内存在高优先级插队机制,对 useEffect 的调度可能被打断,出现与预期不符合的情况

说明#

依然使用 React hooks: hooks 链表 内的示例代码。

  function UseEffectAnduseLayoutEffect() {
    const [text, setText] = useState(0);

    useEffect(() => {
      console.log('useEffect create');
      
      return () => {
        console.log('useEffect destroy');
      }
    }, [text]);

    useLayoutEffect(() => {
      console.log('useLayoutEffect create');

      return () => {
        console.log('useLayoutEffect destroy');
      }
    }, [text]);

    return (
      <div onClick={ () => { setText(1) } }>{ text }</div>
    )
  }

根据之前的调试经验我们知道,每一个 hook 被调用时都会调用 mountWorkInProgressHook 去创建一个 hook 对象,并根据调用顺序构成一个链表结构,且该 hook 链表会被挂载到对应 fiber 对象的 memoizedState 属性上。

而除此之外每个不同类型的 hook 的逻辑都不相同,这里只对 useEffect 进行分析。但在此之前我们先对其结构的称呼做出约定。

  use(Layout)Effect(() => { // 约定其第一个参数为 create
    return () => { // 约定 create 函数的返回值为 destroy

    }
  }, []); // 约定其第二个参数为 依赖

effect 链表#

构建 effect 链表发生在 beginWork 内。

image

hook 会根据组件是挂载还是更新执行不同的逻辑,对于 use (Layout) Effect 来说在挂载时会调用 mount (Layout) Effect,更新时会调用 update (Layout) Effect。

挂载时生成对应的 effect 链表#

我们先对例子中的 useEffect 打入断点后会进入 mountEffect 内的 mountEffectImpl。

  mountEffectImpl(
    UpdateEffect | PassiveEffect, // react 中关于权限存在大量的位操作,此处的**或**操作常有赋予权限的意味
    HookPassive,
    create,
    deps,
  );

  function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
    /**
     * hook 链表内有分析过因此跳过
    */
    const hook = mountWorkInProgressHook();
    const nextDeps = deps === undefined ? null : deps;
    /**
     * A |= B => A = A | B
    */
    currentlyRenderingFiber.flags |= fiberFlags;
    /**
     * 此处的逻辑是一处重点,涉及到 effect 对象的创建与链表的连接
     * 还需要注意到的 useEffect 对应的 hook 对象上的 memoizedState 会挂载上 effect 链表
     * useLayoutEffect 的逻辑和 useEffect 大致相同,只是会对 effect 对象打上不同的标签而已
    */
    hook.memoizedState = pushEffect(
      HookHasEffect | hookFlags,
      create,
      undefined,
      nextDeps,
    );
  }

pushEffect 是构成 effect 链表的关键,其内生成的 effect 环状链表会被挂载到当前组件的 fiber 上的 updateQueue 上,并且 updateQueue.lastEffect 会指向最新生成的 effect 的对象。

image

effect 对象结构如下

  const effect: Effect = {
    tag,
    create, // use(Layout)Effect 的 create
    destroy, // use(Layout)Effect 的 destroy
    deps, // use(Layout)Effect 的 依赖
    // Circular
    next: (null: any), // 指向下一个effect
  };

执行完 mountEffectImpl 后,生成的 effect 链表会被挂载到两处,而 useEffect 此时也运行结束

  1. use (Layout) Effect 对应 hook 元素的 memoizedState
  2. fiber.updateQueue

而接下来运行的 useLayoutEffect 也与上述步骤一致,因此 effect 链表会变成下面这样

image

事实上也确实如此

image

至此挂载阶段生成 effect 链表的相关逻辑已经结束了。

更新时生成对应的 effect 链表#

保持原有的断点,然后触发点击回调,此时会进入 updateEffect 的 updateEffectImpl

  updateEffectImpl(
    UpdateEffect | PassiveEffect,
    HookPassive,
    create,
    deps,
  )

  function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
    const hook = updateWorkInProgressHook(); // 更新情况下,会对 currentHook 进行赋值
    const nextDeps = deps === undefined ? null : deps;
    let destroy = undefined;

    if (currentHook !== null) {
      const prevEffect = currentHook.memoizedState; // 获取 currentHook 对应的 effect 对象
      destroy = prevEffect.destroy; // 将上一次的 destroy 函数赋值给 wip 并在这一次调用
      if (nextDeps !== null) {
        const prevDeps = prevEffect.deps;
        if (areHookInputsEqual(nextDeps, prevDeps)) { // 一一比较新旧的dep,不过是浅对比
          pushEffect(hookFlags, create, destroy, nextDeps);
          return;
        }
      }
    }

    currentlyRenderingFiber.flags |= fiberFlags;

    hook.memoizedState = pushEffect(
      HookHasEffect | hookFlags, // HookHasEffect 标记更新
      create,
      destroy,
      nextDeps,
    );
  }

和 mount 不同,update 的 pushEffect 会带上上一次的 destroy 函数。

关于 useEffect 内 create 与 destroy 运行顺序#

此时我们对 useEffect 的 create 函数打上断点来查看它的调用栈

image

此时我们注意到 flushPassiveEffectsImpl 这一函数,该函数在 flushPassiveEffects 内以 NormalSchedulerPriority 级别的优先级被调度,因此 flushPassiveEffectsImpl 会是一个异步任务,且优先级不高。但当前例子不需要考虑被插队的情况。

通过对 flushPassiveEffects 进行全局的查找(线索是 pendingPassiveHookEffectsMount 的赋值),最后定位调用位置为 commitBeforeMutationEffects

查询异步操作的调用很麻烦,但最后还是定位在了 commitRoot ——> commitRootImpl ——> commitBeforeMutationEffects。该函数是 BeforeMutation 阶段的入口。

收集 effect#

在执行 flushPassiveEffectsImpl 清空 effect 前,首先需要对 fiber 上的 effect 链表进行收集,这一操作发生在 schedulePassiveEffects 内。由于 schedulePassiveEffects 是在 commitLayoutEffects ——> commitLifeCycles 内被调用,因此可以视为在 layout 阶段内被调用

  /**
   * 此处的 finishedWork 为 UseEffectAnduseLayoutEffect 的 fiber 对象
  */
  function schedulePassiveEffects(finishedWork: Fiber) {
    /**
     * fiber 的 updateQueue上挂载了对应的 effect 链表
    */
    const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
    const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
    if (lastEffect !== null) {
      // lastEffect这一指针是指向最后一个 effect 对象上的,因此通过 next 可以直接获取到第一个元素
      const firstEffect = lastEffect.next;
      let effect = firstEffect;
      do {
        const {next, tag} = effect;
        if (
          (tag & HookPassive) !== NoHookEffect && // 过滤了非 use(Layout)Effect 创建的 effect 对象
          (tag & HookHasEffect) !== NoHookEffect // 过滤了依赖未发生变动的 effect 对象
        ) {
          enqueuePendingPassiveHookEffectUnmount(finishedWork, effect); // 给pendingPassiveHookEffectsUnmount赋值
          enqueuePendingPassiveHookEffectMount(finishedWork, effect);// 给pendingPassiveHookEffectsMount赋值
          // 目前执行完 pendingPassiveHookEffectsUnmount 与 pendingPassiveHookEffectsMount数据一致
        }
        effect = next;
      } while (effect !== firstEffect); // 环状链表循环一圈停止
    }
  }

执行 effect#

直接对 flushPassiveEffectsImpl 进行调试,下面的代码已经删除了多余的部分。该函数内部涉及到了对 effect 对象上的 create 与 destroy 执行的逻辑。

function flushPassiveEffectsImpl() {
  ...
  /**
   * 执行 effect 的 destroy 函数
   * 在挂载时,由于在执行 pushEffect 时第三个参数为 undefined,effect 对象上的 destroy 属性为空。因此 destroy 不会被执行
   * 但是在更新时,会传入由 create 返回的 destroy 函数,因此会执行。下面会提到。
  */
  const unmountEffects = pendingPassiveHookEffectsUnmount;
  pendingPassiveHookEffectsUnmount = [];
  for (let i = 0; i < unmountEffects.length; i += 2) {
    const effect = ((unmountEffects[i]: any): HookEffect);
    const fiber = ((unmountEffects[i + 1]: any): Fiber);
    const destroy = effect.destroy;
    effect.destroy = undefined;

    if (typeof destroy === 'function') {
      if (
        enableProfilerTimer &&
        enableProfilerCommitHooks &&
        fiber.mode & ProfileMode
      ) {
        try {
          startPassiveEffectTimer();
          destroy();
        } finally {
          recordPassiveEffectDuration(fiber);
        }
      } else {
        destroy();
      }
    }
  }
  /**
   * 执行 effect 的 create 函数
  */
  const mountEffects = pendingPassiveHookEffectsMount;
  pendingPassiveHookEffectsMount = [];
  for (let i = 0; i < mountEffects.length; i += 2) {
    const effect = ((mountEffects[i]: any): HookEffect);
    const fiber = ((mountEffects[i + 1]: any): Fiber);

    const create = effect.create;
    if (
      enableProfilerTimer &&
      enableProfilerCommitHooks &&
      fiber.mode & ProfileMode
    ) {
      try {
        startPassiveEffectTimer();
        /**
         * create 的返回值是 destroy,如果这个 effect 对象因为 update 再次被收集后,
         * 会在下一次flushPassiveEffectsImpl时被指向
        */
        effect.destroy = create(); 
      } finally {
        recordPassiveEffectDuration(fiber);
      }
    } else {
      effect.destroy = create();
    }

  }
  ...

  return true;
}

结论#

  1. useEffect 是异步操作,清空副作用的操作(flushPassiveEffectsImpl)会被注册为一个 Task
  2. layout 阶段中 effect 会被收集进执行数组,但是调度发生在 before Mutation 调度
  3. 清空副作用的操作(flushPassiveEffectsImpl)的调度优先级不高,所以有可能在调度时有可能被更高优先级的任务插队
  4. 挂载时,只会执行 create 的函数;更新时,会执行 currentHook 的 destroy,然后执行 newHook 的 create

关于 useLayoutEffect 的运行时机#

useLayoutEffect 除了触发时机,其他表现与 useEffect 是一致的。

此时我们对 useLayoutEffect 的 create 函数打上断点来查看它的调用栈

image

此时对 commitHookEffectListMount 打断点,可发现都是同步操作。(commitLayoutEffects 是 layout 的入口)

function commitHookEffectListMount(tag: number, finishedWork: Fiber) {
  const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
  const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
  if (lastEffect !== null) {
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
      // 传入的 tag 是 HookLayout | HookHasEffect,因此可过滤出 useLayoutEffect 的 effect
      if ((effect.tag & tag) === tag) { 
        // Mount
        const create = effect.create;
        effect.destroy = create(); 
      }
      effect = effect.next;
    } while (effect !== firstEffect); // 循环一圈
  }
}

如果对 useLayoutEffect 的 destroy 函数打上断点

image

此时对 commitHookEffectListUnmount 打断点,可发现都是同步操作。(commitMutationEffects 是 mutation 的入口)

function commitHookEffectListUnmount(tag: number, finishedWork: Fiber) {
  const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
  const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
  if (lastEffect !== null) {
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
      if ((effect.tag & tag) === tag) {
        // Unmount
        const destroy = effect.destroy;
        effect.destroy = undefined;
        if (destroy !== undefined) {
          destroy();
        }
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}

结论#

  1. useLayoutEffect 是同步调用的,其 destroy 在 mutation 阶段调用,其 create 在 layout 阶段调用
Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.