React中useEffect的源码解读

news2024/12/29 10:14:41

对源码的解读有利于搞清楚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 ()=>{
    // 卸载时的逻辑
  }
},[])

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

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

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/614887.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JavaWeb之Servlet

1、什么是JavaWeb? Servlet 是 JavaEE 规范之一。规范就是接口 Servlet 就 JavaWeb 三大组件之一。三大组件分别是:Servlet 程序、Filter 过滤器、Listener 监听器。 Servlet 是运行在服务器上的一个 java 小程序,它可以接收客户端发送过来的…

Python plt; ax 设置tick

Python中绘图可以基于plt;也可基于ax 在 Matplotlib 中,Axes 对象(常简写为 ax)是在图(Figure)中进行大部分的绘图操作的地方。一个 Axes 对象代表了一个具体的绘图区域。 利用 plt 绘图 简单的图像测试…

继续探索Roop(单张图视频换脸)的各方面:比如喜闻乐见的“加速”

文章目录 (一)Roop项目的特点(二)Roop也能加速***(三)Roop更新和依赖(3.1)飞速更新(3.2)依赖问题(3.3)需要CUDA么 前两天写了&#x1…

如何修复vcruntime140.dll文件?多种解决vcruntime140.dll的方法分享

在使用Windows操作系统时,经常会遇到一些错误提示,比如缺少vcruntime140.dll文件。这个文件是Visual C Redistributable Package的一部分,它负责运行C程序。如果你在运行某些软件或游戏时收到了“缺少vcruntime140.dll文件”的错误提示&#…

电子科技大学计算机系统结构半期考试参考答案

2023 答案-半期试题(15分) 1、试分析采用哪种设计方案实现求浮点数乘法FPMUL对系统性能提高更大。假定FPMUL操作占整个测试程序执行时间的10%。 一种设计方案是增加专门的FPMUL硬件,可以将FPMUL操作的速度加快到10倍&#…

IP协议与ethernet协议

一、IP协议 1.IP协议作用和意义 (1)计算机网络体系结构 (2)网络互联使用路由器 (3)IP网的意义 当互联网上的主机进行通信时,就好像在一个网络上通信一样,看不见互连的各具体的网络…

Vue.js 中的渲染函数是什么?如何使用渲染函数?

Vue.js 中的渲染函数是什么?如何使用渲染函数? Vue.js 是一款流行的前端框架,它提供了许多方便的工具和 API,用于构建交互式的用户界面。其中,渲染函数是 Vue.js 中一个强大的工具,它可以让我们以编程的方…

深度解析java异步多线程优化版

快速使用需求:我不要理解一堆理论想直接用 操作说明 ITask.java PutEsTask.java TaskExecutor.java TaskQueue.java TestMain.java请把这几个类文件复制下去,运行testMain的方法,根据TestMain的运行日志,【1】-> 【8】不同需求…

2.3 网络设计与redis、memcached、nginx组件

目录 一、网络模块需要处理哪些事情二、reactor网络设计模型三、网络模块与业务的关系四、redis、memcached、nginx1、redis2、memcached3、ngnix4、总结 一、网络模块需要处理哪些事情 网络编程主要关注客户端与服务端交互的四个问题: 1、连接建立 2、消息到达 3、…

《不要挑战人性》笔记(一)

恒河猴实验 代母实验 将刚出生的小猴子与母亲分开,让它与绒布妈妈跟铁丝妈妈生活在一起。铁丝妈妈身上有食物,绒布没有食物,小猴子更喜欢绒布妈妈,即使它在铁丝妈妈那里得到了食物。绒布妈妈身上设置机关,攻击小猴子&a…

OAuth2 工作流程详解

我们之前谈到了使用saml作为SSO单点登录认证。本文讲解oauth2.0协议,oauth2.0协议避免了客户端直接访问受保护资源 什么是OAuth OAuth是一种安全的开放协议,用于在不相关的服务之间授权用户。换句话说,它使一个服务能够访问托管在其他服务上…

Flutter 笔记 | GetX

官网:https://pub.dev/packages/get 中文文档:https://github.com/jonataslaw/getx/blob/master/README.zh-cn.md 关于 GetX 现在Flutter的状态管理方案很多,redux、bloc、state、provider、Getx。 provider是官方提供的状态管理解决方案&…

小程序分包详解

一:什么是小程序分包 分包指的是把一个完整的小程序项目,按照需求划分为不同的子包,在构建时打包成不同的分包,用户在使用时按需进行加载。 二:为什么使用分包 小程序第一版 推出 小程序代码包不能超过1MB 大小&…

java SSM 教代会审批管理系统myeclipse开发mysql数据库springMVC模式java编程计算机网页设计

一、源码特点 java SSM 教代会管理系统是一套完善的web设计系统(系统采用SSM框架进行设计开发,springspringMVCmybatis),对理解JSP java编程开发语言有帮助,系统具有完整的源代码 和数据库,系统主要采用…

计算机网络开荒2-应用层

文章目录 一、网络应用体系结构2.1 客户机/服务器结构(Client-Server,C/S)2.2 点对点结构(Peer-to-peer,P2P)2.3 混合结构(Hybrid) 二、网路应用的进程通信2.1 套接字Socket2.2 应用层协议的内容 三、网络应用的需求与传输层服务四、Web4.1 HTTP4.1.1 HTTP连接类型4.1.1.1 非持…

mysql慢sql优化实战:in order by优化处理

背景 线上查询慢的问题日益突出,专门写一个帖子记录一下处理过程,会定期更新优化处理方案 套餐余量统计查询菜单,数据库查询时间一分钟五十秒,优化之后耗时109毫秒,性能提升很大.所有时间统计均以数据库层面进行统计.用户使用层面因为有数据传输、带宽、业务逻辑处理等因素…

只要7分钟,教你从0-1搭建BI系统,这份大佬整理的指南免费送!

在BI行业干了快十年了,做过制造业供应链的BI项目,也跟过零售电商、房地产、银行的BI建设,经常有朋友咨询关于BI建设的内容,今天总结了一套BI项目建设指南,直接分享给大家,觉得不错记得点赞收藏。 通常情况…

实战【金融评分卡】

金融评分卡 网站:百融、同盾 1.导入 信贷评分卡是一种用于评估个人或企业申请贷款的工具,就像-个评分表-样。我们可以把它看作是银行或金融机构用来判断某人是否有资格获得贷款的一-种方式。 想象一下,你是一个银行家,有很多人向你申请贷款,比如个人贷…

cas单点登录-服务端部署

一.需求描述 公司开发系统越来越多,每个系统都有自己的登录认证流程,给用户很繁琐的体验,统一认证单点登录迫在眉睫 二.流程图 三.本地运行cas服务端 1.拉取cas服务端代码,切换到对应分支(我使用的是6.4.x&#xff…

proxmox7.4 安装后配置

关于proxmox7安装和配置的博文,倒是不少。可惜鱼龙混杂,踩了不少坑,如今实践成功后,做一分享。 proxmox7.4安装(与安装linux系统类似) 1.去官网https://proxmox.com/en/downloads 下载安装镜像,注意: 下…