前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【useEffect原理】源码调试吃透REACT-HOOKS(二)

【useEffect原理】源码调试吃透REACT-HOOKS(二)

作者头像
源心锁
发布2022-08-12 11:53:28
8930
发布2022-08-12 11:53:28
举报
文章被收录于专栏:前端魔法指南前端魔法指南

【useEffect原理】源码调试吃透REACT-HOOKS(二)

1 导读

大家好,我是心锁,一枚23届准毕业生。

上一章,我们了解了hook是怎么赋予函数式组件状态的,也同时借此了解与调试了useState的源码,而这次,我们要动手了解useEffect是如何工作的。

上次我们在开始抛出了五个问题,目前其实解决了三个半

那么这次我们把#5过了

不过开始之前,我们还要抛出几个其他问题:

  1. useEffect的副作用做了什么优化?
  2. useEffect触发的时机是什么时候,副作用清除又在什么时候?

从这里开始,我们一一解答。

2 一些关键信息

2.1 调试代码

本次调试使用到的代码如下

代码语言:javascript
复制
const CountButton = () => {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(v => v + 1);
  };

  useEffect(() => {
    console.log('Hello Mount Effect');
    return () => {
      console.log('Hello Unmount Effect');
    };
  }, []);
  useEffect(() => {
    console.log('Hello count Effect');
  }, [count]);
  return (
    <>
      <div>Render by state</div>
      <div>{count}</div>
      <button onClick={handleClick}>Add Count</button>
    </>
  );
};

2.2 关闭严格模式

!!! 请注意:需要关闭StrictMode,否则React18中的useEffect会执行两次

2.3 前置知识点与提要

为了便于理解useEffect的作用原理,我整理了一些可能需要用到的前置知识点/提要

  • 由于useEffect的作用流在render与commit阶段都存在,我们需要简单知道一下从render阶段进入commit的关键函数是commitRoot,换句话说commit阶段开始于commitRoot
  • Lane这个词汇有关的变量统一可以先忽略,这Scheduler(调度器)有关,简而言之我还没看我也不懂
  • Fiber.finishedWork保存的是每个Fiber的DOM操作依据,这里是在render阶段生成的,目的是避免在commit阶段再遍历一次Fiber树,保存的形式是单向链表和useEffect没有太大关系
  • Fiber.updateQueue,updateQueue的结构是 { lastEffect: null, stores: null } 复制代码 其中updateQueue.lastEffect中保存的是函数式组件中调用useEffect生成的effecteffect的具体结构见下文,保存的形式和state类似是单向环状链表

3 useEffect做了什么

3.1 副作用的挂载(render阶段)

useEffect回调的执行时机并不在render阶段,所以render阶段主要在做的事是存储副作用

3.1.1 mountHookTypesDev

根据堆栈来看,useEffect目前处于beginWork->renderWithHooks->CountButton中,此时的运行均处于completeWork之前

那么此时的useEffect我们可以用上一章的内容快速理解,显然mountHookTypesDev仍是一个将hook类型放入hookTypes的过程。

3.1.2 mountEffect

往下我们来到关键点mountEffect,这里会进一步调用mountEffectImpl

我们继续往下来到mountEffectImpl(我们此时并不知道PassiveXX这种变量是什么意义,但是没关系我们先跳过)

直到往下来到mountEffectImpl的主函数,我们可以知道fiberFlags===Passive|PassiveStatic,hookFlags===Passive$1

由于我们上一章有讲过mountWorkInProgressHook,已经知道这里就是hook保存使用的方法,所有本次直接来到hook.memoizedState=pushEffect(...)

3.1.3 pushEffect‼️

pushEffect第一步声明的结构体effect存储着所有的副作用,这里的各个参数分别代表

  • tag: mount时的入参为HasEffect|hookFlags,猜测和workInProgress.tag不是同一个含义,暂无需理解含义
  • create: useEffect的第一个参数
  • destory: 暂时未知,在mount时我们得到的destory是undefined
  • deps: useEffect的第二个参数
  • next: 来自React的经典链表式存储指针next
代码语言:javascript
复制
function pushEffect(tag, create, destroy, deps) {
  var effect = {
    tag: tag,
    create: create,
    destroy: destroy,
    deps: deps,
    // Circular
    next: null
  };
  ...
  return effect;
}

这之后,这些effect都会被挂载到currentlyRenderingFiber$1.updateQueue更新队列上(上一章我们讲到currentlyRenderingFiber$1===workInProgressFiber)

代码语言:javascript
复制
function pushEffect(tag, create, destroy, deps) {
  ...
  var componentUpdateQueue = currentlyRenderingFiber$1.updateQueue;

  if (componentUpdateQueue === null) {
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    currentlyRenderingFiber$1.updateQueue = componentUpdateQueue;
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {
    var lastEffect = componentUpdateQueue.lastEffect;

    if (lastEffect === null) {
      componentUpdateQueue.lastEffect = effect.next = effect;
    } else {
      var firstEffect = lastEffect.next;
      lastEffect.next = effect;
      effect.next = firstEffect;
      componentUpdateQueue.lastEffect = effect;
    }
  }

  return effect;
}

而显然,从上述代码我们可以看到,effectstate一样,都是以单向环形链表的形式存储

往后即返回,从pushEffect的返回值看,新增的effect将挂载在hook.memoizedState

那么截止这里,我们了解到了副作用的收集过程。


3.1.4 updateEffect*

我们上次调试useState时对于update的流程有了解,需要关注updateWorkInProgressHook的话可以点击这里查看。所以这里简单说说就行。

代码语言:javascript
复制
function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  let destroy = undefined;

  if (currentHook !== null) {
    const prevEffect = currentHook.memoizedState;
    destroy = prevEffect.destroy;
    if (nextDeps !== null) {
      const prevDeps = prevEffect.deps;
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
        return;
      }
    }
  }
  ...
}

我们唯一需要关注的就是areHookInputsEqual,这里做了一个数组遍历比较依赖项是否更新


3.2 副作用的作用(commit阶段)

useEffect回调与副作用清理都在commit阶段,我们在useEffect中打上断点,然后回溯堆栈找到相关的函数

3.2.1 flushPassiveEffects

commit阶段中第一个和effect相关的即flushPassiveEffects,注意如图调用的地方,这里是通过Schedule模块进行调度的,从执行结果看,useEffect将被异步调用。注意此时的阶段

从字面意思,这里做的操作是刷新被动效果

从代码上看,flushPassiveEffects的作用是通过**setCurrentUpdatePriority**设置优先级,然后调用**flushPassiveEffectsImpl**

关于优先级的再说,我们现在来到flushPassiveEffectsImpl

3.2.2 flushPassiveEffectsImpl‼️

flushPassiveEffectsImpl的核心代码在这里,这里做了两件事情:

  • 调用该useEffect在上一次render时的返回的销毁函数
  • 调用该useEffect在本次render时传入的函数
代码语言:javascript
复制
function flushPassiveEffectsImpl() {
  ...
  var root = rootWithPendingPassiveEffects;
  {
    markPassiveEffectsStarted(lanes);
  }
  
  commitPassiveUnmountEffects(root.current);
  commitPassiveMountEffects(root, root.current);
  
  {
    markPassiveEffectsStopped();
  }
  ...
  return true;
}
#1 先执行上一次副作用产生的destory函数
代码语言:javascript
复制
function commitPassiveUnmountEffects(finishedWork) {
  setCurrentFiber(finishedWork);
  commitPassiveUnmountOnFiber(finishedWork);
  resetCurrentFiber();
}

下钻到commitPassiveUnmountEffects,这里是两部分,一个是setCurrentFiberresetCurrentFiber这里会不断把current指向当前Fiber节点然后执行effect,在执行之后则重置current

而另一部分commitPassiveUnmountOnFiber,则会根据Fiber节点不同的tag执行相应的代码

(当然不管是哪种类型目前来看都会执行recursivelyTraversePassiveUnmountEffects

recursivelyTraversePassiveUnmountEffects是做的是递归调用的操作,会从根节点不断向下遍历Fiber节点的子节点。不过recursivelyTraversePassiveUnmountEffects内部执行的逻辑我们现在不需要关心,对于useEffect来说,我们需要关注的是commitHookEffectListUnmount

对于函数式组件,需要执行updateQueue,区分于effectListeffect指代的一般是DOM操作,commitHookEffectListUnmount的过程实际上就是执行pushEffect时塞入updateQueueeffect的过程。

代码语言:javascript
复制
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) {
          // 区分是Effect还是LayoutEffect,做开始标记
          if ((flags & Passive$1) !== NoFlags$1) {
            markComponentPassiveEffectUnmountStarted(finishedWork);
          } else if ((flags & Layout) !== NoFlags$1) {
            markComponentLayoutEffectUnmountStarted(finishedWork);
          }
          ...
          safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy);
          ...
          // 区分是Effect还是LayoutEffect,做停止标记
          if ((flags & Passive$1) !== NoFlags$1) {
            markComponentPassiveEffectUnmountStopped();
          } else if ((flags & Layout) !== NoFlags$1) {
            markComponentLayoutEffectUnmountStopped();
          }
        }
      }

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

遍历updateQueue的过程中,react会不断取出destory并清除effect链条上的**destory**,如果destory不为空则执行

那么在此总结一下,我们知道了useEffect副作用销毁函数的时机,具体就是每次渲染,会先执行上一次useEffect生成的destory函数

#2 执行本次副作用的create

UnMount类似,我们来到commitPassiveMountEffects,往下走同样是一个递归调用commitPassiveMountOnFiberrecursivelyTraversePassiveMountEffects的过程

这里我们只关心commitHookEffectListMount

而对于commitHookEffectListMount,基本操作都是相同的,主要的区别在于其一,会有一个把create即我们传入useEffect的第一个回调的返回值挂载到effect上,为下一次副作用预备好副作用清除函数

其二则是react中提供的一些熟悉的错误告警比如不要在useEffect中直接传入异步函数这一点

(这里又一点学到了,还有typeof destroy.then === 'function'这种判断Promise对象/async函数的方式)

那么致此,useEffect相关的调用结束

4 总结

回到我们一开始抛出的问题,现在我们知道了

  • useEffect触发的时机是什么时候,副作用清除又在什么时候?

useEffect在render阶段做pushEffect的操作,这时会把副作用存储进updateQueue

而在commit阶段则会通过Scheduler协调器异步执行updateQueue,先调用destory清除上次的副作用,再调用本次的create`生成新的副作用


  • useEffect的副作用做了什么优化?

异步执行,上述我们也看到了,useEffect通过Scheduler异步执行,根据官方说法,在React17后,useEffect异步执行,因为大部分副作用不需要延迟屏幕更新。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-08-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 【useEffect原理】源码调试吃透REACT-HOOKS(二)
    • 1 导读
      • 2 一些关键信息
        • 2.1 调试代码
        • 2.2 关闭严格模式
        • 2.3 前置知识点与提要
      • 3 useEffect做了什么
        • 3.1 副作用的挂载(render阶段)
        • 3.2 副作用的作用(commit阶段)
      • 4 总结
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档