解读useEffect和useLayouEffect原理

1,477 阅读7分钟

背景

写这篇文章是因为工作上不是非常繁忙,可以抽空学习自己常用框架和类库,深入理解它们,在技术上希望有更大的进步,培养学习兴趣;

useEffect

和其它hooks一样,加载和更新执行不一样的方法(mountEffect和updateEffect);

1.mountEffect

页面加载时,执行mountEffect;

  1. 创建hook对象,加入组件的hook单向链表;
  2. 在组件的fiber的flag中加入副作用相关的effectTag;(加载期间默认有layoutEffect和effect的副作用)
  3. 创建effect对象,给hook对象的memoizedState和加入组件fiber的updateQueue中形成effect环状链表;在渲染工作完成后,会循环这个环状链表,执行每个effect对象的destory和create;
const effect = { tag, create, destroy, deps, next: null };
tag 是effect的类型 tag为9是useEffect, 5是useLayoutEffect
create是 useEffect或useLayoutEffect的回调函数
destroy是 create返回的回调函数
deps是useEffect或useLayoutEffect的依赖数组
next指向下个effect对象;

1.1.effect环状链表图

function pushEffect(tag, create, destroy, deps){
  const effect = { tag, create, destroy, deps, next: null };
  //新创建的effect对象为最后为effect链表的一个effect对象,componentUpdateQueue.lastEffect会指向新创建的effect对象
  //新创建的effect对象的next会指向第一个effct对象;
  let componentUpdateQueue = (currentlyRenderingFiber.updateQueue);
  if(componentUpdateQueue === null){
    //当前没有updateQueue
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    //创建updateQueue
    currentlyRenderingFiber.updateQueue = componentUpdateQueue;
    //形成一个环状链表
    componentUpdateQueue.lastEffect = effect.next = effect
  }else{
    const lastEffect = componentUpdateQueue.lastEffect;
    if(lastEffect === null){
      componentUpdateQueue.lastEffect = effect.next = effect;
    }else{
      //第一个effect对象为最先创建的的effect对象
      const firstEffect = lastEffect.next; //获取第一个effect对象
      lastEffect.next = effect;// 旧的最后一个effect对象的next,指向新创建的effect
      effect.next = firstEffect;// 新创建的effect对象的next指向第一个effect
      componentUpdateQueue.lastEffect = effect; // updateQueue的lastEffect指向effect,新创建的effect变为最后一个effect对象
    }
  }
  return effect;
}

2.updateEffect

页面更新时,执行updateEffect;

  1. 根据hook单向链表获取对应的更新时的hook对象,创建新的hook对象,加入hook单向链表;

  2. 如果effect的deps不为null,或者undefined,会从当前hook对象拿到上一次effect对象,再从effect对象拿到deps和destroy,用新的deps与之比较;

    1. 如果新老deps相等,push一个不带HookHasEffect的tag给effect对象,加入updateQueue环状链表(这个effect不会被标记为有副作用,所以,effect的create和destroy不会被执行),不更新hook.memoizedState;
    2. 如果新老deps不相等,更新effect对象,在effect的tag中加入HookHasEffect和上一次create执行的destroy,更新hook.memoizedState;

3.useEffct的回调函数和销毁函数的执行时机

在render时期构建effect链表;在commit时执行先执行之前没有执行完的useEffect,然后,在beforeMutation阶段操作dom前,以NormalPriority常规优先级添加一个异步任务到任务队列(这个异步任务是用来执行useEffect的destroy和create的),在layout阶段完成,页面完成渲染后,执行在beforeMutation阶段添加的异步任务;

3.1.commit开始时

主要是为了执行之前没有执行的useEffect

进入commit阶段,这和useEffect异步调度的特点有关,它以一般的优先级被调度,意味着一旦有更高优先级的任务进入到commit阶段,上一次任务的useEffect还没得到执行。所以在本次更新开始前,需要先将之前的useEffect都执行掉,以保证本次调度的useEffect都是本次更新产生的。

function commitRootImpl(root, recoverableErrors, renderPriorityLevel) {
  do {
      // `flushPassiveEffects` will call `flushSyncUpdateQueue` at the end, which
      // means `flushPassiveEffects` will sometimes result in additional
      // passive effects. So we need to keep flushing in a loop until there are
      // no more pending effects.
      // TODO: Might be better if `flushPassiveEffects` did not automatically
      // flush synchronous work at the end, to avoid factoring hazards like this.
      flushPassiveEffects();
    } while (rootWithPendingPassiveEffects !== null);
  ...省略代码
}

3.2.beforeMutation

只会发起一次useEffect调度,是异步调度,以NormalPriority常规优先级添加一个异步任务在任务队列中(push(timerQueue, newTask)),在页面渲染完成时,会执行这个异步任务

function commitRootImpl(root, recoverableErrors, renderPriorityLevel) {
  ...省略代码
  if ((finishedWork.subtreeFlags & PassiveMask) !== NoFlags || (finishedWork.flags & PassiveMask) !== NoFlags) {
      if (!rootDoesHavePassiveEffects) {
        rootDoesHavePassiveEffects = true;
        scheduleCallback$1(NormalPriority, function () {
          //添加一个异步任务到任务队列
          flushPassiveEffects(); // This render triggered passive effects: release the root cache pool
          // *after* passive effects fire to avoid freeing a cache pool that may
          // be referenced by a node in the tree (HostRoot, Cache boundary etc)

          return null;
        });
      }
   } 
  ...省略代码
}

3.3.layout

加载时,只执行useEffect的create函数即可;

如果pendingPassiveEffectsLanes是同步赛道,就在页面渲染完直接执行useEffect的create和destroy,在beforeMutation时添加的异步任务,不会执行useEffect的create和destory

if (includesSomeLane(pendingPassiveEffectsLanes, SyncLane) && root.tag !== LegacyRoot) {
  //加载期间默认是不走这里的
  //这里也是执行useEffect的create,如果pendingPassiveEffectsLanes是同步赛道,
  //就在渲染完成后直接执行useEffect的create和destory
  //在beforeMutation时添加的异步任务执行时,不会执行useEffect的create和destory
    flushPassiveEffects();
  }

执行上一次useEffect的create返回的destroy,拿到函数组件fiber的updateQueue,循环这个effect环状链表,拿到effect对象的destroy执行;

function commitHookEffectListUnmount(flags, finishedWork, nearestMountedAncestor) {
  var updateQueue = finishedWork.updateQueue;
  var lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;

  if (lastEffect !== null) {
    var firstEffect = lastEffect.next;
    var effect = firstEffect;

    do {
      if ((effect.tag & flags) === flags) {
        // Unmount
        var destroy = effect.destroy;
        effect.destroy = undefined;

        if (destroy !== undefined) {
          {
            if ((flags & Passive$1) !== NoFlags$1) {
              markComponentPassiveEffectUnmountStarted(finishedWork);
            } else if ((flags & Layout) !== NoFlags$1) {
              markComponentLayoutEffectUnmountStarted(finishedWork);
            }
          }

          safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy);//执行destroy

          {
            if ((flags & Passive$1) !== NoFlags$1) {
              markComponentPassiveEffectUnmountStopped();
            } else if ((flags & Layout) !== NoFlags$1) {
              markComponentLayoutEffectUnmountStopped();
            }
          }
        }
      }

      effect = effect.next;
    } while (effect !== firstEffect);
  }
}

执行完所有组件的destroy,再执行create;同理,也是拿到函数组件fiber的updateQueue,循环这个effect环状链表,拿到effect对象的create执行,然后把create返回的destroy给effect对象(留着下着更新执行useEffect时用);

function commitHookEffectListMount(flags, finishedWork) {
  var updateQueue = finishedWork.updateQueue;
  var lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;

  if (lastEffect !== null) {
    var firstEffect = lastEffect.next;
    var effect = firstEffect;

    do {
      if ((effect.tag & flags) === flags) {
        {
          if ((flags & Passive$1) !== NoFlags$1) {
            markComponentPassiveEffectMountStarted(finishedWork);
          } else if ((flags & Layout) !== NoFlags$1) {
            markComponentLayoutEffectMountStarted(finishedWork);
          }
        } // Mount


        var create = effect.create;
        effect.destroy = create();

        {
          if ((flags & Passive$1) !== NoFlags$1) {
            markComponentPassiveEffectMountStopped();
          } else if ((flags & Layout) !== NoFlags$1) {
            markComponentLayoutEffectMountStopped();
          }
        }

        {
          var destroy = effect.destroy;

          if (destroy !== undefined && typeof destroy !== 'function') {
            var hookName = void 0;

            if ((effect.tag & Layout) !== NoFlags) {
              hookName = 'useLayoutEffect';
            } else if ((effect.tag & Insertion) !== NoFlags) {
              hookName = 'useInsertionEffect';
            } else {
              hookName = 'useEffect';
            }

            var addendum = void 0;

            if (destroy === null) {
              addendum = ' You returned null. If your effect does not require clean ' + 'up, return undefined (or nothing).';
            } else if (typeof destroy.then === 'function') {
              addendum = '\n\nIt looks like you wrote ' + hookName + '(async () => ...) or returned a Promise. ' + 'Instead, write the async function inside your effect ' + 'and call it immediately:\n\n' + hookName + '(() => {\n' + '  async function fetchData() {\n' + '    // You can await here\n' + '    const response = await MyAPI.getData(someId);\n' + '    // ...\n' + '  }\n' + '  fetchData();\n' + "}, [someId]); // Or [] if effect doesn't need props or state\n\n" + 'Learn more about data fetching with Hooks: https://reactjs.org/link/hooks-data-fetching';
            } else {
              addendum = ' You returned: ' + destroy;
            }

            error('%s must not return anything besides a function, ' + 'which is used for clean-up.%s', hookName, addendum);
          }
        }
      }

      effect = effect.next;
    } while (effect !== firstEffect);
  }
}

3.4.useEffect执行图

4.思考的问题

4.1.mountEffect和updateEffect的不同

  1. mountEffect会默认给effect对象的tag加入HookHasEffect;而updateEffect要判断新老deps是否相同,在依赖有变时,才会给effect对象的tag加入HookHasEffect;
  2. mountEffect时,effect的destroy为undefined;而updateEffect的destroy为上一次create执行时的返回值;
  3. mountEffect是创建新的hook对象;而updateEffect依据上一次hook对象,创建新的hook对象;
  4. mountEffect的hook.memoizedState是新的;而updateEffect要在依赖更新时,会更新hook.memoizedState,依赖不更新时,hook.memoizedState不更新;

4.2.effect链表会保存在hook.memoizedState和fiber.updateQueue

在hook.memoizedState时就是记录当前effect的状态,方便下一次updateEffect时,获取上一次effect的状态(会用到上一次effect的deps,destroy等);

在updateQueue时,在commit阶段处理,判断effect.tag,执行effect.destroy和effect.create;

4.3.为什么要所有(组件)的销毁函数执行完,才能执行所有的回调函数?

有的变量或者ref是多个组件共用的;如果a组件的销毁函数改变了ref.current,而b和c组件的回调函数需要用到ref.current,如果a组件的销毁函数早于b组件的回调函数,又晚于c组件的回调函数,执行的结果就不统一,所以,所有的销毁函数要早于所有的回调函数执行;(在某个组件useEffect的销毁函数中修改的ref.current可能影响另一个组件useEffect的回调函数中的同一个ref的current属性。在useLayoutEffect中也有同样的问题,所以他们都遵循“全部销毁”再“全部执行”的顺序。)

useLayoutEffect和useEffect的相同点与不同点

1.相同点

  1. 在render阶段,都要创建effect对象和hook对象,加入到hook.memoizedState和fiber.updateQueue形成环状链表;
  2. 在commit阶段,destroy和create的执行方式是一样的,都是循环fiber.updateQueue,拿到effect对象的destroy和create执行,先执行所有组件的destroy,然后执行create;

2.不同点

  1. 在render阶段,创建的effect对象的tag属性值不同,用来区分useEffect和useLayoutEffect;
  2. 在commit阶段:
    1. 执行的时机不同,useLayoutEffect的destroy是在commitMutationEffects中,组件的dom操作完,commitWork中执行;create在commitLayoutEffects中执行;而useEffect的destroy和create执行要晚于useLayoutEffect,在页面渲染完成后执行;
    2. useLayoutEffect均是同步操作,执行destroy会阻塞页面渲染,useEffect有可能异步,可能同步,不会阻塞页面渲染;

3.useLayout执行图

render阶段与useEffect相同

function commitLayoutEffectOnFiber(finishedRoot, current, finishedWork, committedLanes) {
  if ((finishedWork.flags & LayoutMask) !== NoFlags) {
    switch (finishedWork.tag) {
      case FunctionComponent:
      case ForwardRef:
      case SimpleMemoComponent:
        {
          if ( !offscreenSubtreeWasHidden) {
            // At this point layout effects have already been destroyed (during mutation phase).
            // This is done to prevent sibling component effects from interfering with each other,
            // e.g. a destroy function in one component should never override a ref set
            // by a create function in another component during the same commit.
            if ( finishedWork.mode & ProfileMode) {
              try {
                startLayoutEffectTimer();
                //执行所有的useLayoutEffect的create
                commitHookEffectListMount(Layout | HasEffect, finishedWork);
              } finally {
                recordLayoutEffectDuration(finishedWork);
              }
            } else {
              commitHookEffectListMount(Layout | HasEffect, finishedWork);
            }
          }
        }
    }
    ...

总结

1.学习方法

  1. 查阅网上的相关文章,对原理有了一个大概的理解;
  2. 写了一个dome,进行多次调试debug;
  3. 专注于当下,当前学的是useEffect的原理,但源码上会有很多与这次学习无关的代码,需要把它们忽略掉,这样不会浪费额外的精力;
  4. 一遍学不会,就第二遍,学习也是一个循环的过程;书读百遍其义自见,总能学会;不可丢失学习的热情与信心;
  5. 可能流程太长,很难理解,可以分解成单个函数甚至单行代码,把知识分细一点,我们一点点攻克它;

参考资料

segmentfault.com/a/119000003…

react.iamkasong.com/renderer/be…