架构篇commit阶段流程概览我们讲解了useEffect的工作流程。

其中我们谈到

flushPassiveEffects方法内部会从全局变量rootWithPendingPassiveEffects获取effectList

本节我们深入flushPassiveEffects方法内部探索useEffect的工作原理。

flushPassiveEffectsImpl

flushPassiveEffects内部会设置优先级,并执行flushPassiveEffectsImpl

你可以从这里useEffect - 图1 (opens new window)看到flushPassiveEffects的代码

flushPassiveEffectsImpl主要做三件事:

  • 调用该useEffect在上一次render时的销毁函数

  • 调用该useEffect在本次render时的回调函数

  • 如果存在同步任务,不需要等待下次事件循环宏任务,提前执行他

本节我们关注前两步。

v16中第一步是同步执行的,在官方博客useEffect - 图2 (opens new window)中提到:

副作用清理函数(如果存在)在 React 16 中同步运行。我们发现,对于大型应用程序来说,这不是理想选择,因为同步会减缓屏幕的过渡(例如,切换标签)。

基于这个原因,在v17.0.0中,useEffect的两个阶段会在页面渲染后(layout阶段后)异步执行。

事实上,从代码中看,v16.13.1中已经是异步执行了

接下来我们详细讲解这两个步骤。

阶段一:销毁函数的执行

useEffect的执行需要保证所有组件useEffect销毁函数必须都执行完后才能执行任意一个组件的useEffect回调函数

这是因为多个组件间可能共用同一个ref

如果不是按照“全部销毁”再“全部执行”的顺序,那么在某个组件useEffect销毁函数中修改的ref.current可能影响另一个组件useEffect回调函数中的同一个refcurrent属性。

useLayoutEffect中也有同样的问题,所以他们都遵循“全部销毁”再“全部执行”的顺序。

在阶段一,会遍历并执行所有useEffect销毁函数

  1. // pendingPassiveHookEffectsUnmount中保存了所有需要执行销毁的useEffect
  2. const unmountEffects = pendingPassiveHookEffectsUnmount;
  3. pendingPassiveHookEffectsUnmount = [];
  4. for (let i = 0; i < unmountEffects.length; i += 2) {
  5. const effect = ((unmountEffects[i]: any): HookEffect);
  6. const fiber = ((unmountEffects[i + 1]: any): Fiber);
  7. const destroy = effect.destroy;
  8. effect.destroy = undefined;
  9. if (typeof destroy === 'function') {
  10. // 销毁函数存在则执行
  11. try {
  12. destroy();
  13. } catch (error) {
  14. captureCommitPhaseError(fiber, error);
  15. }
  16. }
  17. }

其中pendingPassiveHookEffectsUnmount数组的索引i保存需要销毁的effecti+1保存该effect对应的fiber

pendingPassiveHookEffectsUnmount数组内push数据的操作发生在layout阶段 commitLayoutEffectOnFiber方法内部的schedulePassiveEffects方法中。

commitLayoutEffectOnFiber方法我们在Layout阶段已经介绍

  1. function schedulePassiveEffects(finishedWork: Fiber) {
  2. const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
  3. const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
  4. if (lastEffect !== null) {
  5. const firstEffect = lastEffect.next;
  6. let effect = firstEffect;
  7. do {
  8. const {next, tag} = effect;
  9. if (
  10. (tag & HookPassive) !== NoHookEffect &&
  11. (tag & HookHasEffect) !== NoHookEffect
  12. ) {
  13. // 向`pendingPassiveHookEffectsUnmount`数组内`push`要销毁的effect
  14. enqueuePendingPassiveHookEffectUnmount(finishedWork, effect);
  15. // 向`pendingPassiveHookEffectsMount`数组内`push`要执行回调的effect
  16. enqueuePendingPassiveHookEffectMount(finishedWork, effect);
  17. }
  18. effect = next;
  19. } while (effect !== firstEffect);
  20. }
  21. }

阶段二:回调函数的执行

与阶段一类似,同样遍历数组,执行对应effect回调函数

其中向pendingPassiveHookEffectsMountpush数据的操作同样发生在schedulePassiveEffects中。

  1. // pendingPassiveHookEffectsMount中保存了所有需要执行回调的useEffect
  2. const mountEffects = pendingPassiveHookEffectsMount;
  3. pendingPassiveHookEffectsMount = [];
  4. for (let i = 0; i < mountEffects.length; i += 2) {
  5. const effect = ((mountEffects[i]: any): HookEffect);
  6. const fiber = ((mountEffects[i + 1]: any): Fiber);
  7. try {
  8. const create = effect.create;
  9. effect.destroy = create();
  10. } catch (error) {
  11. captureCommitPhaseError(fiber, error);
  12. }
  13. }