• 从React源码分析看useEffect


    热身准备

    这里不再讲useLayoutEffect,它和useEffect的代码是一样的,区别主要是:

    • 执行时机不同;
    • useEffect是异步, useLayoutEffect是同步,会阻塞渲染;

    初始化 mount

    mountEffect

    在所有hook初始化时都会通过下面这行代码实现hook结构的初始化和存储,这里不再讲mountWorkInProgressHook方法

    var hook = mountWorkInProgressHook();
    
    • 1

    mountEffect方法中,只有这几行代码。先来解读下几个参数:

    • fiberFlags:有副作用的更新标记,用来标记hook所在的fiber
    • hookFlags:副作用标记;
    • create:使用者传入的回调函数;
    • deps:使用者传入的数组依赖;
    function mountEffectImpl(fiberFlags, hookFlags, create, deps) {
      // hook初始化
      var hook = mountWorkInProgressHook();
      // 判断是否有传入deps,如果有会作为下次更新的deps
      var nextDeps = deps === undefined ? null : deps;
      // 给hook所在的fiber打上有副作用的更新的标记
      currentlyRenderingFiber$1.flags |= fiberFlags;
      // 将副作用操作存放到fiber.memoizedState.hook.memoizedState中
      hook.memoizedState = pushEffect(HasEffect | hookFlags, create, undefined, nextDeps);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    上面代码中都有注释,接下来我们看看React是如何存放副作用更新操作的,主要就是pushEffect方法

    function pushEffect(tag, create, destroy, deps) {
      // 初始化副作用结构,
      var effect = {
        tag: tag,
        create: create,   // 回调函数
        destroy: destroy,  // 回调函数里的return(mount时是undefined)
        deps: deps,    // 依赖数组
        // 闭环链表
        next: null
      };
      // 下面的一大段代码看着复杂,但是有没有很熟悉的感觉?
      var componentUpdateQueue = currentlyRenderingFiber$1.updateQueue;
    
      if (componentUpdateQueue === null) {
        componentUpdateQueue = createFunctionComponentUpdateQueue();
        currentlyRenderingFiber$1.updateQueue = componentUpdateQueue;
        // effect.next = effect形成环形链表
        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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    上面这段代码除了初始化副作用的结构代码外,都是我们前面讲过的操作闭环链表,向链表末尾添加新的effect,该effect.next指向fisrtEffect,并且链表当前的指针指向最新添加的effect

    useEffect的初始化就这么简单,简单总结一下:给hook所在的fiber打上副作用更新标记,并且fiber.memoizedState.hook.memoizedStatefiber.updateQueue存储了相关的副作用,这些副作用通过闭环链表的结构存储。

    更新 update

    updateEffect

    updateWorkInProgressHook在上篇文章也已讲过,不再详述,主要功能就是创建一个带有回调函数的newHook去覆盖之前的hook

    function updateEffectImpl(fiberFlags, hookFlags, create, deps) {
      var hook = updateWorkInProgressHook();
      var nextDeps = deps === undefined ? null : deps;
      var destroy = undefined;
    
      if (currentHook !== null) {
        var prevEffect = currentHook.memoizedState;
        destroy = prevEffect.destroy;
    
        if (nextDeps !== null) {
          var prevDeps = prevEffect.deps;
          // 比较两次依赖数组中的值是否有变化
          if (areHookInputsEqual(nextDeps, prevDeps)) {
            // 和之前初始化时一样
            pushEffect(hookFlags, create, destroy, nextDeps);
            return;
          }
        }
      }
      // 和之前初始化时一样
      currentlyRenderingFiber$1.flags |= fiberFlags;
      hook.memoizedState = pushEffect(HasEffect | hookFlags, create, destroy, nextDeps);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    相信眼眼尖的看官已经注意到上面代码中有两个pushEffect,一个没有赋值给hook.memoizedState,一个赋值了,这两者有什么区别呢?

    先保留着这个疑问,先来了解下下面这行代码都做了些什么,因为它造就了两个pushEffect

    if (areHookInputsEqual(nextDeps, prevDeps)){...}

    function areHookInputsEqual(nextDeps, prevDeps) {
      // 没有传deps的情况返回false
      if (prevDeps === null) {
        return false;
      }
      // deps不是[],且其中的值有变动才会返回false
      for (var i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
        if (objectIs(nextDeps[i], prevDeps[i])) {
          continue;
        }
        return false;
      }
      // deps = [],或者deps里面的值没有变化会返回true
      return true;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    它会判断两次依赖数组中的值是否有变化以及deps是否是空数组来决定返回truefalse,返回true表明这次不需要调用回调函数。

    现在我们明白了两次pushEffect的异同,if内部的pushEffect是不需要调用的回调函数, 外面的pushEffect是需要调用的。再来仔细看下这两行代码:

    // if内部的,第一个参数是hookFlags = 4
    pushEffect(hookFlags, create, destroy, nextDeps);
    // if外部的,第一个参数是HasEffect | hookFlags = 5
    hook.memoizedState = pushEffect(HasEffect | hookFlags, create, destroy, nextDeps);
    
    • 1
    • 2
    • 3
    • 4

    相关参考视频讲解:进入学习

    这两行代码的区别是传入的第一个参数不同,而第一个参数就是effect.tag的值,effect.tag = 4不会添加到副作用执行队列,而effect.tag = 5可以。没有添加到副作用执行队列的effect就不会执行。这样就巧妙的实现了useEffect基于deps来判断是否需要执行回调函数。

    到这里, 我们搞明白了,不管useEffect里的deps有没有变化都会为回调函数创建effect并添加到effect链表和fiber.updateQueue中,但是React会根据effect.tag来决定该effect是否要添加到副作用执行队列中去执行。

    执行副作用

    我们现在知道了,useEffect是异步执行的。那么这个回调函数副作用会在什么时候执行呢?useEffect回调函数会在layout阶段之后执行。现在我们来了解下具体调用执行的流程。

    image.png

    我画了一个简单的流程图,大致描述了下调用流程。首先在mutation之前阶段,基于副作用创建任务并放到taskQueue中,同时会执行requestHostCallback,这个方法就涉及到了异步了,它首先考虑使用MessageChannel实现异步,其次会考虑使用setTimeout实现。使用MessageChannel时,requestHostCallback会马上执行port.postMessage(null);,这样就可以在异步的第一时间执行workLoopworkLoop会遍历taskQueue,执行任务,如果是useEffecteffect任务,会调用flusnPassiveEffects

    Q:可能有人会疑惑为什么优先考虑MessageChannel

    A: 首先我们要明白React调度更新的目的是为了时间分片,意思是每隔一段时间就把主线程还给浏览器,避免长时间占用主线程导致页面卡顿。使用MessageChannelSetTimeout的目的都是为了创建宏任务,因为宏任务会在当前微任务都执行完后,等到浏览器主线程空闲后才会执行。不优先考虑setTimeout的原因是,setTimeout执行时间不准确,会造成时间浪费,即使是setTimeout(fn, 0),感兴趣的可以去自己了解下,本文不做赘述了。

    schedulePassiveEffects中,会决定是否执行effect链表中的effect,判断的依据就是每个effect上的effect.tag:

    function schedulePassiveEffects(finishedWork) {
      var updateQueue = finishedWork.updateQueue;
      var lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
    
      if (lastEffect !== null) {
        var firstEffect = lastEffect.next;
        var effect = firstEffect;
        // 遍历effect链表
        do {
          var _effect = effect,
              next = _effect.next,
              tag = _effect.tag;
          // 基于effect.tag决定是否添加到副作用执行队列
          if ((tag & Passive$1) !== NoFlags$1 && (tag & HasEffect) !== NoFlags$1) {
            enqueuePendingPassiveHookEffectUnmount(finishedWork, effect);
            enqueuePendingPassiveHookEffectMount(finishedWork, effect);
          }
    
          effect = next;
        } while (effect !== firstEffect);
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    flushPassiveEffects中,会先执行上次更新动作的销毁函数,然后再执行本次更新动作的回调函数,并且会把回调函数的return作为下次更新动作的销毁函数。

    function flushPassiveEffectsImpl() {
      // 执行上次更新动作的销毁函数
      var unmountEffects = pendingPassiveHookEffectsUnmount;
      pendingPassiveHookEffectsUnmount = [];
      for (var i = 0; i < unmountEffects.length; i += 2) {
        ...destroy()
      }
      // 执行本次更新动作的回调函数
      var mountEffects = pendingPassiveHookEffectsMount;
      pendingPassiveHookEffectsMount = [];
      for (var _i = 0; _i < mountEffects.length; _i += 2) {
        ...create()
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    上面代码中的这两行就是来自副作用执行队列,已经过滤掉了不需要执行的effect,只执行该队列上的副作用函数

    var unmountEffects = pendingPassiveHookEffectsUnmount;
    
    var mountEffects = pendingPassiveHookEffectsMount;
    
    • 1
    • 2
    • 3

    总结

    看完这篇文章, 我们可以弄明白下面这几个问题:

    1. useEffectuseLayoutEffect的区别?
    2. useEffect是怎么判断回调函数是否需要执行的?
    3. useEffect是同步还是异步?
    4. useEffect是通过什么实现异步的?
    5. useEffect为什么要要优先选用MessageChannel实现异步?
  • 相关阅读:
    mysql源码分析——InnoDB的内存结构源码
    RabbitMQ安装与配置
    Matter理论教程-通用-1-01:理论详解
    MySQL进阶实战1,数据类型与三范式
    javaee之黑马乐优商城5
    Redis的缓存更新策略和缓存问题
    【编程题】【Scratch四级】2022.06 成绩查询
    浅谈C++函数
    设计模式(14)备忘录模式
    函数题42 习题11-2 查找星期《C语言程序设计(第4版)》题目集
  • 原文地址:https://blog.csdn.net/It_kc/article/details/128143594