• React中useEffect的源码解读


    前言

    对源码的解读有利于搞清楚Hooks到底做了什么,如果您觉得useEffect很“魔法”,这篇文章也许对您有些帮助。

    本篇博客篇幅有限,只看useEffect,力求简单明了,带您到React Hooks的深处看看

    按图索骥找到Hook相关源码(可以直接跳)

    首先我们从Github上得到react的源码,然后可以在packages中找到react文件夹,其中的index.js就是我们的入口。

    代码很简单,就两行:

    const React = require('./src/React');
    
    module.exports = React.default || React;
    

    所以接下来我们去看看 'react/src/React',代码有点多,我们简化一下:

    import ReactVersion from 'shared/ReactVersion';
    
    // ...
    
    import {
      useEffect,
    } from './ReactHooks';
    
    const React = {
      useEffect
    };
    
    //...
    export default React;
    

    很好,现在我们至少知道为什么Hooks的引用方式是:

    import {useEffect} from 'react'

    接下来我们继续看看 'react/src/ReactHooks'。

    ReactHooks文件(可以直接跳)

    之前说好了只看useEffect,所以同样需要简化一下代码。

    并且考虑到有人对TypeScript语法不熟悉,还去掉了TypeScript语法,之后的简化代码也会如此。

    现在我们看下简化后的代码:

    import invariant from 'shared/invariant';
    
    import ReactCurrentDispatcher from './ReactCurrentDispatcher';
    
    function resolveDispatcher() {
      const dispatcher = ReactCurrentDispatcher.current;
      // React版本不对或者Hook使用有误什么的就报错
      // ...
      return dispatcher;
    }
    
    export function useEffect(create,inputs) {
      const dispatcher = resolveDispatcher();
      return dispatcher.useEffect(create, inputs);
    }
    

    这里可以看到,我们的useEffect实际上是ReactCurrentDispatcher.current.useEffect。

    ReactCurrentDispatcher文件(可以直接跳)

    看一下ReactCurrentDispatcher文件,这里没有简化:

    import type {Dispatcher} from 'react-reconciler/src/ReactFiberHooks';
    
    const ReactCurrentDispatcher = {
      current: (null: null | Dispatcher),
    };
    
    export default ReactCurrentDispatcher;
    

    发现他的current的类型是null或者Dispatcher,所以这里我们很简单就能猜到,这个东西的源码在 'react-reconciler/src/ReactFiberHooks'。

    ReactFiberHooks文件

    几千行代码,头大。但是莫慌,咱们又不是来写react的,看看原理而已。

    我们之前已经知道useEffect实际上是ReactCurrentDispatcher.current.useEffect。

    很明显ReactCurrentDispatcher.current不管是什么东西单独列出来,我们只需要知道谁赋值给他就行了。

    精简代码,去掉用__DEV__区分的开发代码之后,我们发现整个文件给ReactCurrentDispatcher.current赋值的没几个。

    而唯一一个与异常判断无关的是renderWithHooks函数中的这一块代码:

    export function renderWithHooks(
      current,
      workInProgress,
      Component,
      props,
      secondArg,
      nextRenderExpirationTime
    ){
      ReactCurrentDispatcher.current =
          current === null || current.memoizedState === null
            ? HooksDispatcherOnMount
            : HooksDispatcherOnUpdate;
      let children = Component(props, secondArg);
      return children;
    }
    

    我们不知道这段代码是干嘛的,但是他肯定是渲染组件时用的。

    而这里很显然ReactCurrentDispatcher.current的值就只能是HooksDispatcherOnMount和HooksDispatcherOnUpdate。

    很明显这两个一个用于加载时,一个用于更新时。

    然后我们们搜一下相关代码:

    const HooksDispatcherOnMount = {
      useEffect: mountEffect
    };
    
    const HooksDispatcherOnUpdate = {
      useEffect: updateEffect
    };
    

    也就是说,组件加载时,useEffect会调用mountEffect,组件更新时会调用updateEffect。

    让我们继续看看这两个函数:

    function mountEffect(create, deps) {
      return mountEffectImpl(
        UpdateEffect | PassiveEffect,
        UnmountPassive | MountPassive,
        create,
        deps,
      );
    }
    
    function updateEffect(create, deps) {
      return updateEffectImpl(
        UpdateEffect | PassiveEffect,
        UnmountPassive | MountPassive,
        create,
        deps,
      );
    }
    

    这里的UpdateEffect和PassiveEffect是二进制常数,用位运算的方式操作。

    先不用知道具体意义,知道是个常量即可。

    接下来我们看看具体的mountEffectImpl:

    function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps){
      const hook = mountWorkInProgressHook();
      // useEffect不传依赖,那么就为null
      const nextDeps = deps === undefined ? null : deps;
      currentlyRenderingFiber.effectTag |= fiberEffectTag;
      // 链表尾部hook对象的memoizedState为pushEffect的返回值
      hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps);
    }
    

    我们看到第一行代码调用mountWorkInProgressHook新建了一个hook对象,让我们看看mountWorkInProgressHook:

    function mountWorkInProgressHook() {
      const hook = {
        memoizedState: null,
    
        baseState: null,
        baseQueue: null,
        queue: null,
    
        next: null,
      };
    
      if (workInProgressHook === null) {
        // This is the first hook in the list
        currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
      } else {
        // Append to the end of the list
        workInProgressHook = workInProgressHook.next = hook;
      }
      return workInProgressHook;
    }
    

    很明显这里有个链表结构workInProgressHook,如果workInProgressHook链表为null就将新建的hook对象赋值给它,如果不为null,那么就加在链表尾部。

    这里有必要讲解一下:

    Hooks作为一个链表存储在fiber的memoizedState中。
    currentHook 是当前fiber的链表。
    workInProgressHook 是即将被加入到 work-in-progress fiber的链表。

    然后我们再看看pushEffect:

    function pushEffect(tag, create, destroy, deps) {
      // 新建一个effect,很明显又是个链表结构
      const effect = {
        tag,
        create,
        destroy,
        deps,
        // Circular
        next: null,
      };
      // 从currentlyRenderingFiber.updateQueue获取组件更新队列
      let componentUpdateQueue= currentlyRenderingFiber.updateQueue;
      // 判断组件更新队列是否为空,每次在调用renderWithHooks都会将这个componentUpdateQueue置为null
      // 这样的话每次update这个组件时,就会创建一个新的effect链表
      if (componentUpdateQueue === null) {
        // 为空就创建一个组件更新队列
        componentUpdateQueue = createFunctionComponentUpdateQueue();
        // 并赋值给currentlyRenderingFiber.updateQueue
        currentlyRenderingFiber.updateQueue = componentUpdateQueue;
        // 组件更新队列最新的effect为我们新建的effect
        componentUpdateQueue.lastEffect = effect.next = effect;
      } else {
        // 如果组件更新队列已经存在,获取它最新的Effect
        const lastEffect = componentUpdateQueue.lastEffect;
        if (lastEffect === null) {
          // 如果最新的Effect为null,那么组件更新队列最新的Effect为我们新建的effect
          componentUpdateQueue.lastEffect = effect.next = effect;
        } else {
          // 否则将我们的effect加入到链表结构中最末尾,然后他的next为链表结构的第一个effect
          // 这里的effect链表是个闭环
          const firstEffect = lastEffect.next;
          lastEffect.next = effect;
          effect.next = firstEffect;
          componentUpdateQueue.lastEffect = effect;
        }
      }
      return effect;
    }
    

    我们再看看更新时调用的updateEffectImpl:

    function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps) {
      //  这里 updateWorkInProgressHook 
      //  workInProgressHook = workInProgressHook.next;
      //  currentHook = currentHook.next;
      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;
          // 对比两个依赖数组的各个值之间是否有变动,如果没变动,那么就设置标志位为NoHookEffect
          if (areHookInputsEqual(nextDeps, prevDeps)) {
            pushEffect(NoHookEffect, create, destroy, nextDeps);
            return;
          }
        }
      }
    
      currentlyRenderingFiber.effectTag |= fiberEffectTag;
    
      hook.memoizedState = pushEffect(hookEffectTag, create, destroy, nextDeps);
    }
    

    我们可以看到updateEffectImpl和mountEffectImpl很像,最重要的是我们得两个函数串起来,看看他们到底实现了一个什么。

    Hook相关数据结构简图

    这里我自己画了一张图,利于理解:

    这张图的结构是一个组件某一时刻的结构。

    图中黄色为Fiber节点,绿色为Hook节点,蓝色为Effect节点。

    Fiber节点,其实就是我们的虚DOM节点,react会生成一个Fiber节点树,每个组件在Fiber树上都有对应的Fiber节点。

    其中currentlyRenderingFiber表示我们正在进行渲染的节点,它来自于workInProgress,current表示已经渲染的节点。

    组件加载时,会执行各个useEffect,然后就会建立一个Hook链表,而workInProgress的memoizedState字段就指向了Hook链表的尾部Hook节点。

    而构建每个Hook节点时,会同时构造一个Effect节点,同样,Hook节点的memoizedState字段就指向了对应的Effect节点。

    而每个Effect节点又会连接起来形成一个链表,然后workInProgress的updateQueue字段指向了Effect链表的尾部Effect节点。

    组件更新时,会依次对比currentHook指向的Effect的依赖数组与新的依赖数组的不同,如果一样,就设置Effect节点的effectTag为NoHookEffect。

    但是无论依赖数组中的值是否改变,都会新构造一个Effect节点,作为Hook节点的memoizedState字段的值。

    然后在准备渲染时,会去直接找到Fiber节点的updateQueue的lastEffect,也就是直接指向Effect链表的尾部Effect节点。

    因为effect链表是闭环的,这里通过lastEffect的next找到第一个Effect。

    然后循环遍历effect链表,当effectTag为NoHookEffect则不做操作,否则会去先执行effect的destroy操作,然后再执行create操作。

    对,你没看错,总结起来就是每次更新后,只要依赖项改变,那么就会执行useEffect的卸载函数,再执行第一个参数create函数。

    这一部分代码比较远:

    function commitHookEffectList(
      unmountTag,
      mountTag,
      finishedWork,
    ) {
      const updateQueue = finishedWork.updateQueue;
      let lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
      if (lastEffect !== null) {
        const firstEffect = lastEffect.next;
        let effect = firstEffect;
        do {
          if ((effect.tag & unmountTag) !== NoHookEffect) {
            // Unmount
            const destroy = effect.destroy;
            effect.destroy = undefined;
            if (destroy !== undefined) {
              destroy();
            }
          }
          if ((effect.tag & mountTag) !== NoHookEffect) {
            // Mount
            const create = effect.create;
            effect.destroy = create();
          }
          effect = effect.next;
        } while (effect !== firstEffect);
      }
    }
    

    这里的位运算大家可能有点看不懂,因为NoHookEffect的值是0,所以只要effect.tag被设置为NoHookEffect,那么

    effect.tag & unmountTag
    

    就必然为NoHookEffect。

    我们还记得,我们之前的玩法,依赖数组各个值不变时,就设置Effect节点的effectTag为NoHookEffect。

    此时是绝对不会执行先destroy Effect节点,再执行Effect函数create的操作。

    而如果effect.tag的值不为NoHookEffect,那也得需要effect.tag与unmountTag至少有一个位相同才能执行destroy。

    让我们看看之前无论是mountEffectImpl还是updateEffectImpl都默认传的是:UnmountPassive | MountPassive,也就是说effect.tag为UnmountPassive | MountPassive。

    而很明显这个设计的目的在于,当mountTag为MountPassive时执行create函数,而unmountTag为UnmountPassive时创建执行destroy函数。

    而只有下面这个地方会做这个Passive操作:

    export function commitPassiveHookEffects(finishedWork: Fiber): void {
      if ((finishedWork.effectTag & Passive) !== NoEffect) {
        switch (finishedWork.tag) {
          case FunctionComponent:
          case ForwardRef:
          case SimpleMemoComponent:
          case Chunk: {
            commitHookEffectList(UnmountPassive, NoHookEffect, finishedWork);
            commitHookEffectList(NoHookEffect, MountPassive, finishedWork);
            break;
          }
          default:
            break;
        }
      }
    }
    

    这里的意思很明显,先遍历一遍effect链表,每个依赖项变了的hook都destroy一下,然后再遍历一遍effect链表,每个依赖项变了的,都执行create函数一下。

    也就是说每次都会按照useEffect的调用顺序,先执行所有useEffect的卸载函数,再执行所有useEffect的create函数。

    而commitPassiveHookEffects又是只有flushPassiveEffects这个函数最终能调用到。

    而每次 React 在检测到数据变化时,flushPassiveEffects就会执行。

    不论是props还是state的变化都会如此。

    所以如果您真的有需要去模拟一个像之前的componentDidMount和componentWillUnmount的生命周期,那么最好用上一个单独的Effect:

    useEffect(()=>{
      // 加载时的逻辑
      return ()=>{
        // 卸载时的逻辑
      }
    },[])
    

    这里用[]作为依赖数组,是因为这样依赖就不会变动,也就是只在加载时执行一次加载逻辑,卸载时执行一次卸载逻辑。

    不加依赖数组时,那么每次渲染都会执行一次加载和卸载。

    总结

    希望我的这篇文章让您有一些收获。

    这里主要想说一下自己在源码阅读中的感想。

    读这部分的源码,其实更像是在盲人摸象。

    为了追求效率,需要避开一些比较复杂的东西,比如我们提到的Fiber节点树,又比如其实useEffect替换effect和具体执行effect并不是同步的。

    否则解读很多东西要花很多时间,怕是真的要写一个系列了。

    源码阅读略显艰涩,还好关键的地方都是有注释的,在阅读的过程中也能收获不少东西。

    我并不是react的开发者,如果在解读的过程中有什么疏漏和误解,还望诸位批评指正。

  • 相关阅读:
    JS收集<3>:限制URL
    TSQL小收集<1>:为已经存在的表添加唯一约束
    第一个NHibernate实例 yangan
    用Javascript获取select的值 yangan
    获取页面名称语句 yangan
    asp.net中当服务器出错时显示指定的错误页面,同时把错误信息写入系统日志文件 yangan
    Log4Net使用指南 yangan
    明确自己的首要责任
    《大数据时代》摘录
    2013年3月阅读链接
  • 原文地址:https://www.cnblogs.com/vvjiang/p/12160791.html
Copyright © 2020-2023  润新知