最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • useEffect和useLayoutEffect源码浅析

    正文概述 掘金(ZHANGYU)   2021-02-25   621

    上篇文章useState和useReducer源码浅析

    以下源码浅析的React版本为17.0.1,使用ReactDOM.render创建的同步应用,不含优先级相关。

    数据结构

    Effect类型保存相关信息

    type Effect = {|
      tag: HookFlags, // 标识是useEffect还是useLayoutEffect(HasEffect、Layout、Passive )
      create: () => (() => void) | void, // 回调函数
      destroy: (() => void) | void, // 销毁回调函数
      deps: Array<mixed> | null, // 依赖数组
      next: Effect, // 下一个Effect
    |};
    

    函数组件的Effect信息保存在函数组件Fiber节点的updateQueue字段,updateQueue为一个单向环形链表。

    type FunctionComponentUpdateQueue = {|lastEffect: Effect | null|};
    

    Fiber.updateQueue.lastEffect为最后一个EffectlastEffect.next为第一个Effect

    这里值得注意的是Effect对象除了赋值给了updateQueue,同时也会赋值给Fiber节点中Hooks链表的对应HookmemoizedState属性,用于后续的对比。

    useEffect和useLayoutEffect

    之前的文章里讲到了Mount时和Update时用的Hook不是同一个,但是useEffectuseLayoutEffect在Mount和Update时用的都是同一个方法,只是传入了不同的参数。

    mountEffect和mountLayoutEffect

    // Mount时useEffect
    function mountEffect(
      create: () => (() => void) | void,
      deps: Array<mixed> | void | null,
    ): void {
      return mountEffectImpl(
        UpdateEffect | PassiveEffect, // 赋值给Fiber.flags,与useLayoutEffect时不同的是多了个PassiveEffect
        HookPassive, // 赋值给effect.tag
        create, // 回调函数
        deps, // 依赖数组
      );
    }
    
    // Mount时的useLayoutEffect
    function mountLayoutEffect(
      create: () => (() => void) | void,
      deps: Array<mixed> | void | null,
    ): void {
      return mountEffectImpl(
        UpdateEffect, 
        HookLayout, // 与useEffect不同
        create, 
        deps,
      );
    }
    

    mountEffectmountLayoutEffect内部都是调用了mountEffectImpl,区别只是flags的标识不同,用于区分是useEffect还是useLayoutEffect

    updateEffect和updateLayoutEffect

    // Update时useEffect
    function updateEffect(
      create: () => (() => void) | void,
      deps: Array<mixed> | void | null,
    ): void {
      return updateEffectImpl(
        UpdateEffect | PassiveEffect,
        HookPassive,
        create,
        deps,
      );
    }
    
    // Update时useLayoutEffect
    function updateLayoutEffect(
      create: () => (() => void) | void,
      deps: Array<mixed> | void | null,
    ): void {
      return updateEffectImpl(UpdateEffect, HookLayout, create, deps);
    }
    

    和Mount时一样,Update时useEffectuseLayoutEffect内部使用的相同函数。

    mountEffectImpl

    mountEffectImpl函数里主要给Fiber节点添加了对应的flags,同时处理函数组件里Hooks链表,将Effect对象赋值给对应的workInProgressHookmemoizedState

    function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
      // 处理hooks链表同时返回workInProgressHook
      const hook = mountWorkInProgressHook();
      const nextDeps = deps === undefined ? null : deps;
      // 为当前的Fiber节点添加flags
      currentlyRenderingFiber.flags |= fiberFlags;
      // pushEffect会返回Effect,同时赋值给workInProgressHook的memoizedState属性
      hook.memoizedState = pushEffect(
        HookHasEffect | hookFlags,
        create,
        undefined,
        nextDeps,
      );
    }
    

    内部调用了pushEffect函数来创建Effect对象。

    pushEffect

    pushEffect函数创建了Effect对象,并组装updateQueue的单向环形链表。

    function pushEffect(tag, create, destroy, deps) {
      // effect对象
      const effect: Effect = {
        tag, // effect的tag,用于区分useEffect和useLayoutEffect
        create, // 回调函数
        destroy, // 销毁函数
        deps, // 依赖数组
        // 环形链表
        next: (null: any),
      };
      let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
      // fiber节点不存在updateQueue则需要初始化
      if (componentUpdateQueue === null) {
        // 创建新的updateQueue
        componentUpdateQueue = createFunctionComponentUpdateQueue();
        currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
        // 初始值
        componentUpdateQueue.lastEffect = effect.next = effect;
      } else {
        const lastEffect = componentUpdateQueue.lastEffect;
        if (lastEffect === null) {
          componentUpdateQueue.lastEffect = effect.next = effect;
        } else {
          // 单向环形链表 lastEffect为最新的effect,lastEffect.next为第一个effect
          const firstEffect = lastEffect.next;
          lastEffect.next = effect;
          effect.next = firstEffect;
          componentUpdateQueue.lastEffect = effect;
        }
      }
      return effect;
    }
    

    updateQueue不存在时,调用createFunctionComponentUpdateQueue创建新的updateQueue,否则就将新的effect添加到链表里。

    createFunctionComponentUpdateQueue

    createFunctionComponentUpdateQueue方法创建一个新的updateQueue

    function createFunctionComponentUpdateQueue(): FunctionComponentUpdateQueue {
      return {
        lastEffect: null,
      };
    }
    

    updateEffectImpl

    updateEffectImpl函数内部多了一个判断传入的依赖数组是否相等的判断。

    function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
      // 获取workInProgressHook,改变currentHook和workInProgressHook的指向
      const hook = updateWorkInProgressHook();
      const nextDeps = deps === undefined ? null : deps;
      let destroy = undefined;
    
      if (currentHook !== null) {
        // 上一次的effect对象
        const prevEffect = currentHook.memoizedState;
        destroy = prevEffect.destroy;
        if (nextDeps !== null) {
          // 上一次的依赖数组
          const prevDeps = prevEffect.deps;
          // 判读两个依赖数组是否相同
          if (areHookInputsEqual(nextDeps, prevDeps)) {
            pushEffect(
              hookFlags, // 对应的effect tag少了HookHasEffect,可视作无变化
              create, 
              destroy,  // 和mount时不同传入了destroy
              nextDeps,
            );
            // 直接return
            return;
          }
        }
      }
    
      currentlyRenderingFiber.flags |= fiberFlags;
    
      hook.memoizedState = pushEffect(
        HookHasEffect | hookFlags,
        create,
        destroy,
        nextDeps,
      );
    }
    

    到现在分析了useEffectuseLayoutEffect在Mount和Update时如何创建Effect对象,与useState不同的时,它们不能通过调用dispatchAction来主动触发更新,而是随着useState变化触发更新的同时随着Fiber树的构建在commit阶段执行回调函数和销毁函数。

    但是useEffectuseLayoutEffect的回调函数和销毁函数执行的时机是不同的,这也是它们之间的直接区别。

    useEffect的异步执行

    workInProgressFiber树构建完成,进入commit阶段后,会异步调用useEffect的回调函数和销毁函数。

    commit阶段内部又分为3个阶段

    • Before Mutation阶段
    • Mutation阶段
    • Layout阶段

    发起useEffect调度是在Before Mutation阶段执行的。

    useEffect是异步调度的,需要执行回调函数和销毁函数的useEffect是在Layout阶段执行收集的,所以在最终异步处理useEffect的时候已经收集好了。

    发起useEffect调度

    Before Mutation阶段会执行commitBeforeMutationEffects函数,这个函数同时也会执行类组件的getSnapshotBeforeUpdate生命周期。

    function commitBeforeMutationEffects() {
      // 省略无关代码...
      while (nextEffect !== null) {
        const flags = nextEffect.flags;
        // 当flags包含Passive时表示有调用useEffect
        if ((flags & Passive) !== NoFlags) {
          if (!rootDoesHavePassiveEffects) {
            // 将全局标识赋值为true,一个异步调度就会处理所有的useEffect,避免发起多个
            rootDoesHavePassiveEffects = true;
            // 通过调度器发起一个异步调度
            scheduleCallback(NormalSchedulerPriority, () => {
              // 处理useEffect
              flushPassiveEffects();
              return null;
            });
          }
        }
        // 遍历有副作用的Fiber节点
        nextEffect = nextEffect.nextEffect;
      }
    }
    

    flushPassiveEffects函数内部会调用flushPassiveEffectsImpl函数,在这里会执行回调函数和销毁函数,因为是异步调度的,已经是渲染结束后了。

    收集需要处理的useEffect

    上面说到需要执行回调函数和销毁函数的useEffect是在Layout阶段执行收集的。

    Layout阶段会执行commitLayoutEffects函数,其中flags包含Update的Fiber节点的会执行commitLifeCycles函数。

    function commitLifeCycles(
      finishedRoot: FiberRoot,
      current: Fiber | null,
      finishedWork: Fiber,
      committedLanes: Lanes,
    ): void {
      switch (finishedWork.tag) {
        case FunctionComponent:
        case ForwardRef:
        case SimpleMemoComponent:
        case Block: {
          // 这里执行了useLayoutEffect的回调函数
          commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
          // 收集需要处理的useEffect
          schedulePassiveEffects(finishedWork);
          return;
        }
    	// 省略无关代码...
    }
    

    FunctionComponent会执行schedulePassiveEffects函数,schedulePassiveEffects函数中收集了需要执行回调函数和销毁函数的useEffect

    function schedulePassiveEffects(finishedWork: Fiber) {
      // updateQueue环形链表同时存了useEffect和useLayoutEffect的Effect对象
      const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
      const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
      if (lastEffect !== null) {
        const firstEffect = lastEffect.next;
        let effect = firstEffect;
        // 遍历updateQueue
        do {
          const {next, tag} = effect;
          if (
            // HookPassive标识useEffect
            (tag & HookPassive) !== NoHookEffect &&
            // 当依赖数组没有发生变化时pushEffect的调用没有传入HookHasEffect,所以会被排除
            (tag & HookHasEffect) !== NoHookEffect
          ) {
            // 需要执行销毁函数
            enqueuePendingPassiveHookEffectUnmount(finishedWork, effect);
            // 需要执行回调函数
            enqueuePendingPassiveHookEffectMount(finishedWork, effect);
          }
          effect = next;
        } while (effect !== firstEffect);
      }
    }
    

    需要执行销毁函数和回调函数的Effect对象分别存在两个数组中,数组的偶数下标为Effect对象,奇数下标为Fiber节点。

    // 存需要执行回调函数
    let pendingPassiveHookEffectsMount: Array<HookEffect | Fiber> = [];
    // 存需要执行销毁函数
    let pendingPassiveHookEffectsUnmount: Array<HookEffect | Fiber> = [];
    

    再看如何把Effect对象存入数组的。

    // 需要执行回调函数
    export function enqueuePendingPassiveHookEffectMount(
      fiber: Fiber,
      effect: HookEffect,
    ): void {
      // 一次push两个不同数据,一个Effect对象,一个Fiber节点
      pendingPassiveHookEffectsMount.push(effect, fiber);
    	// 省略代码...
    }
    // 需要执行销毁函数
    export function enqueuePendingPassiveHookEffectUnmount(
      fiber: Fiber,
      effect: HookEffect,
    ): void {
      pendingPassiveHookEffectsUnmount.push(effect, fiber);
    	// 省略代码...
    }
    

    执行回调函数和销毁函数

    这个过程由调度器异步调度执行,执行的函数为flushPassiveEffects的内部函数flushPassiveEffectsImpl

    function flushPassiveEffectsImpl() {
      // 省略代码...
      
      const unmountEffects = pendingPassiveHookEffectsUnmount;
      pendingPassiveHookEffectsUnmount = [];
      // 执行销毁函数destroy函数
      // 偶数下标为HookEffect,奇数下标为fiber节点
      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') {
          try {
            destroy();
          } catch (error) {
            captureCommitPhaseError(fiber, error);
          }
        }
      }
      
      // 执行回调函数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);
        try {
          const create = effect.create;
          effect.destroy = create();
        } catch (error) {
          invariant(fiber !== null, 'Should be working on an effect.');
          captureCommitPhaseError(fiber, error);
        }
      }
      
      // 省略代码...
    }
    

    useLayoutEffect的同步执行

    useEffect不同,useLayoutEffect就完全是同步的了,并且不需要像useEffect一样去收集Effect对象,而是直接通过updateQueue执行。

    useLayoutEffect的回调函数执行在Layout阶段,销毁函数执行在Mutation阶段。

    执行回调函数

    上面说到Layout阶段会执行commitLifeCycles函数。

    function commitLifeCycles(
      finishedRoot: FiberRoot,
      current: Fiber | null,
      finishedWork: Fiber,
      committedLanes: Lanes,
    ): void {
      switch (finishedWork.tag) {
        case FunctionComponent:
        case ForwardRef:
        case SimpleMemoComponent:
        case Block: {
          // 执行useLayoutEffect的回调函数
          commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
          // 收集需要处理的useEffect
          schedulePassiveEffects(finishedWork);
          return;
        }
    	// 省略无关代码...
    }
    

    commitLifeCycles函数里调用了commitHookEffectListMount函数执行useLayoutEffect的回调。

    commitHookEffectListMount

    function commitHookEffectListMount(tag: number, finishedWork: Fiber) {
      // 取出Fiber节点的updateQueue
      const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
      const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
      if (lastEffect !== null) {
        const firstEffect = lastEffect.next;
        let effect = firstEffect;
        // 遍历执行回调函数create函数
        do {
          // (tag => HookLayout | HookHasEffect) 标识effect对象为useLayoutEffect
          if ((effect.tag & tag) === tag) {
            // 执行回调函数
            const create = effect.create;
            effect.destroy = create();
          }
          effect = effect.next;
        } while (effect !== firstEffect);
      }
    }
    

    执行销毁函数

    销毁函数的执行在Mutation阶段,Mutation阶段会执行commitMutationEffects函数,函数内部会对flags包含Update的Fiber节点再执行commitWork函数。

    function commitWork(current: Fiber | null, finishedWork: Fiber): void {
      switch (finishedWork.tag) {
        case FunctionComponent:
        case ForwardRef:
        case MemoComponent:
        case SimpleMemoComponent:
        case Block: {
          // 执行useLayoutEffect的销毁函数
          commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork);
          return;
        }
      }
    }
    

    commitHookEffectListUnmount

    commitHookEffectListUnmount函数和commitHookEffectListMount函数逻辑那还就是一样。

    function commitHookEffectListUnmount(tag: number, finishedWork: Fiber) {
      // 取出Fiber节点的updateQueue
      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) {
            // 执行销毁函数
            const destroy = effect.destroy;
            effect.destroy = undefined;
            if (destroy !== undefined) {
              destroy();
            }
          }
          effect = effect.next;
        } while (effect !== firstEffect);
      }
    }
    

    至此useLayoutEffect执行完毕。

    总结

    useEffectuseLayoutEffect的函数本身在Mount和Update时调用的都是相同的函数,仅参数不同,最大的区别在于useEffect是异步执行,useLayoutEffect是同步执行。

    useEffectuseLayoutEffect所使用的Effect对象储存在函数组件的Fiber节点的updateQueue中,它是一个单向环形链表,updateQueue.lastEffect为最新的Effect对象,lastEffect.next为第一个Effect对象,同时为了维护函数组件的Hooks链表,Effect对象也同时被添加到了Fiber节点的memorizedState属性中。

    Effect对象通过tag字段区分是useEffect还是useLayoutEffectHookPassiveuseEffectHookLayoutuseLayoutEffectHookHasEffect标记Effect的回调和销毁函数需要执行。

    在Fiber树的render阶段通过renderWithHooKS方法执行函数组件,同时会执行内部的Hook,函数组件执行完成后创建了储存Effect对象的updateQueue链表。

    在commit阶段,useEffect会在Before Mutation阶段通过commitBeforeMutationEffects函数发起异步调度,在Layout阶段通过函数commitLayoutEffects将需要执行回调函数和销毁函数的Effect分别收集到pendingPassiveHookEffectsMountpendingPassiveHookEffectsUnmount数组。在commit阶段完毕后会经过调度器执行回调函数和销毁函数。

    useLayoutEffect是同步执行的,它的销毁函数在Mutation阶段通过commitMutationEffects函数最终调用commitHookEffectListUnmount函数执行。它的回调函数会在 Layout阶段通过commitLayoutEffects函数最终调用commitHookEffectListMount函数执行。

    如有错误,还望交流指正。


    起源地下载网 » useEffect和useLayoutEffect源码浅析

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元