React18从0实现dispatch update流程

目录
  • 引言
    • hooks原理
    • useState初始化(mount)
    • mountState
    • mountWorkInProgressHook
    • 处理hook数据
    • 生成dispatch
    • 更新的总结
    • useState触发更新(dispatch)
    • updateState
    • hook数据从哪里来
    • updateWorkInProgressHook
    • hook在条件语句中报错
    • useState计算
    • 双缓存树
  • 总结

引言

本系列是讲述从0开始实现一个react18的基本版本。由于React源码通过Mono-repo 管理仓库,我们也是用pnpm提供的workspaces来管理我们的代码仓库,打包我们使用rollup进行打包。

仓库地址

具体章节代码3个commit

上一节中我们讲解了update的过程中,begionWorkcompleteWorkcommitWork的具体执行流程。本节主要是讲解

  • hooks是如何存放数据的,以及一些hooks的规则。
  • 一次dispatch触发的更新整体流程,双缓存树的运用。

我们有如下代码,在初始化的时候执行useState和调用setNum的时候,是如何更新的。

function App() {
  const [num, setNum] = useState(100);
  window.setNum = setNum;
  return <div>{num}</div>;
}

hooks原理

基于useState我们来讲讲hook在初始化和更新阶段的区别。以及react是如何做到hook不能在条件语句和函数组件外部使用的。

react中,对于同一个hook,在不同的环境都是有不同的集合区分,这样就可以做到基于不同的执行环境的不同判断。

首先有几个名词:

currentlyRenderingFiber: 记录当前正在执行的函数组件的fiberNode

workInProgressHook: 当前正在执行的hook

currentHook:更新的时候的数据来源

memoizedState: 对于fiberNode.memoizedState是存放hooks的指向。对于hook.memoizedState就是存放数据的地方。

hook的结构如下图:

useState初始化(mount)

我们知道当beginWork阶段的时候,对于函数组件,会执行renderWithHooks去生成当前对应的子fiberNode。 我们首先来看看renderWithHooks的逻辑部分。

export function renderWithHooks(wip: FiberNode) {
  // 赋值操作
  currentlyRenderingFiber = wip;
  // 重置
  wip.memoizedState = null;
  const current = wip.alternate;
  if (current !== null) {
    // update
    currentDispatcher.current = HooksDispatcherOnUpdate;
  } else {
    // mount
    currentDispatcher.current = HooksDispatcherOnMount;
  }
  const Component = wip.type;
  const props = wip.pendingProps;
  const children = Component(props);
  // 重置操作
  currentlyRenderingFiber = null;
  workInProgressHook = null;
  currentHook = null;
  return children;
}

首先会将currentlyRenderingFiber赋值给当前的FC的fiberNode,然后重置掉memoizedState, 因为初始化的时候会生成,更新的时候会根据初始化的时候生成。

可以看到对于mount阶段,主要是执行HooksDispatcherOnMount, 他实际上是一个hook集合。我们主要看看mountState的逻辑处理。

const HooksDispatcherOnMount: Dispatcher = {
  useState: mountState,
};

mountState

对于第一次执行useState, 我们根据结果来推算这个函数的主要功能。useState需要返回2个值,第一个是state,第二个是可以引发更新的setState。所以mountState的主要功能:

  • 根据传入的initialState生成新的state
  • 返回dispatch,便于之后调用更新state

基于hook的结构图,我们知道每一个hook有三个属性, 所以我们首先要有一个函数去生成对应的hook的结构。

interface Hook {
  memoizedState: any;
  updateQueue: unknown;
  next: Hook | null;
}

mountWorkInProgressHook

mountWorkInProgressHook这个函数主要是构建hook的数据。分为2种情况,第一种是第一个hook, 第二种是不是第一个hook就需要通过next属性,将hook串联起来。

在这个函数中,我们就可以判断当前执行的hook,是否是在函数中执行的。如果是在函数中执行的话,在执行函数组件的时候,我们将currentlyRenderingFiber 赋值给了wip, 如果是直接调用的话,currentlyRenderingFiber则为null,我们就可以抛出错误。

/**
 * mount获取当前hook对应的数据
 */
function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,
    updateQueue: null,
    next: null,
  };
  if (workInProgressHook === null) {
    // mount时,第一个hook
    if (currentlyRenderingFiber === null) {
      throw new Error("请在函数组件内调用hook");
    } else {
      workInProgressHook = hook;
      currentlyRenderingFiber.memoizedState = workInProgressHook;
    }
  } else {
    // mount时,后续的hook
    workInProgressHook.next = hook;
    workInProgressHook = hook;
  }
  return workInProgressHook;
}

当第一次执行的时候,workInProgressHook的值为null, 说明是第一个hook执行。所以我们将赋值workInProgressHook正在执行的hook, 同时将FC fiberNodememoizedState指向第一个hook。此时就生成了如下图的结构:

处理hook数据

通过mountWorkInProgressHook我们得到当前的hook结构后,需要处理memoizedState以及updateQueue的值。

function mountState<State>(
  initialState: (() => State) | State
): [State, Dispatch<State>] {
  // 找到当前useState对应的hook数据
  const hook = mountWorkInProgressHook();
  let memoizedState;
  if (initialState instanceof Function) {
    memoizedState = initialState();
  } else {
    memoizedState = initialState;
  }
  // useState是可以触发更新的
  const queue = createUpdateQueue<State>();
  hook.updateQueue = queue;
  hook.memoizedState = memoizedState;
  //@ts-ignore
  const dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, queue);
  queue.dispatch = dispatch;
  return [memoizedState, dispatch];
}

从上面的代码中,我们可以看出memoizedState的处理很简单,就是通过传入的参数,进行赋值处理,重点在于如何生成dispatch

生成dispatch

因为触发dispatch的时候,react是要触发更新的,所以必然会和调度有关。

由于要触发更新,我们就需要创建触发更新的队列

  • 执行createUpdateQueue() 生成更新队列。
  • 将更新队列赋值给当前hook保存起来,方便之后update使用。
  • 将生成的dispatch保存起来,方便之后update使用。
// useState是可以触发更新的
const queue = createUpdateQueue<State>();
hook.updateQueue = queue;
const dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, queue);
queue.dispatch = dispatch;

主要是看如何生成dispatch的逻辑,通过调用dispatchSetState它接受三个参数,因为我们需要知道是从哪一个fiberNode开始调度的,所以当前的fiberNode是肯定看需要的。更新队列queue也是需要的,用于执行dispatch的时候触发更新。

function dispatchSetState<State>(
  fiber: FiberNode,
  updateQueue: UpdateQueue<State>,
  action: Action<State>
) {
  const update = createUpdate(action); // 1. 创建update
  enqueueUpdate(updateQueue, update); //  2. 将更新放入队列中
  scheduleUpdateOnFiber(fiber); // 3. 开始调度
}

所以我们每次执行setState的时候,等同于执行上面函数,但是我们只需要传递action就可以,前2个参数,已经通过bind绑定。

执行dispatch后,开始新一轮的调度,调和。

更新的总结

从上面的代码,我们可以看出我们首先是执行了createUpdateQueue, 然后执行了createUpdate, 然后enqueueUpdate。这里总结一下这些函数调用。

createUpdateQueue本质上就创建了一个对象,用于保存值

 return {
   shared: {
     pending: null,
   },
   dispatch: null,
 }

createUpdate就是也是返回一个对象。

return {
  action,
};

enqueueUpdate就是将createUpdateQueue的pending 赋值。

{
  updateQueue.shared.pending = update;
};

最后我们生成的单个hook结构如下图:

useState触发更新(dispatch)

当我们执行setNum(3)的时候,我们之前讲过相当于是执行了下面函数, 将传递3为action的值。

function dispatchSetState<State>(
  fiber: FiberNode,
  updateQueue: UpdateQueue<State>,
  action: Action<State>
) {
  const update = createUpdate(action);
  enqueueUpdate(updateQueue, update);
  scheduleUpdateOnFiber(fiber); // 3. 开始调度
}

当再次执行到函数组件App的时候,会执行renderWithHooks如下的逻辑。将 currentDispatcher.current赋值给HooksDispatcherOnUpdate

// 赋值操作
currentlyRenderingFiber = wip;
// 重置
wip.memoizedState = null;
const current = wip.alternate;
if (current !== null) {
  // update
  currentDispatcher.current = HooksDispatcherOnUpdate;
} else {
  // mount
  currentDispatcher.current = HooksDispatcherOnMount;
}

然后执行App函数,重新会调用useState

const [num, setNum] = useState(100);

updateState

HooksDispatcherOnUpdate中,useState对应的是updateState。对比于mountState的话,updateState主要是:

  • hook的数据从哪里来
  • 会有2种情况执行,交互阶段触发,render的时候触发

本节主要是分析交互阶段的触发的逻辑。

hook数据从哪里来

对比mountState中,我们可以通过新建hook数据结构。这个时候双缓存树的结构就可以解决,还记得我们之前的章节讲的react将正在渲染的和正在进行的分2个树,通过alternate进行链接。整体结构如下图:

还记得我们mount的时候说过,fiberNode.memoizedState的指向保存着hook的数据。

所以我们可以通过currentlyRenderingFiber?.alternate中的memoizedState去查找对应的hook数据。

updateWorkInProgressHook

更新阶段hook的数据获取是通过updateWorkInProgressHook执行的。

function updateWorkInProgressHook(): Hook {
  // TODO render阶段触发的更新
  let nextCurrentHook: Hook | null;
  // FC update时的第一个hook
  if (currentHook === null) {
    const current = currentlyRenderingFiber?.alternate;
    if (current !== null) {
      nextCurrentHook = current?.memoizedState;
    } else {
      nextCurrentHook = null;
    }
  } else {
    // FC update时候,后续的hook
    nextCurrentHook = currentHook.next;
  }
  if (nextCurrentHook === null) {
    // mount / update u1 u2 u3
    // update u1 u2 u3 u4
    throw new Error(
      `组件${currentlyRenderingFiber?.type}本次执行时的Hook比上次执行的多`
    );
  }
  currentHook = nextCurrentHook as Hook;
  const newHook: Hook = {
    memoizedState: currentHook.memoizedState,
    updateQueue: currentHook.updateQueue,
    next: null,
  };
  if (workInProgressHook === null) {
    // update时,第一个hook
    if (currentlyRenderingFiber === null) {
      throw new Error("请在函数组件内调用hook");
    } else {
      workInProgressHook = newHook;
      currentlyRenderingFiber.memoizedState = workInProgressHook;
    }
  } else {
    // update时,后续的hook
    workInProgressHook.next = newHook;
    workInProgressHook = newHook;
  }
  return workInProgressHook;
}

主要逻辑总结如下:

  • 刚开始currentHook为null, 通过alternate指向memoizedState获取到正在渲染中的hook数据,赋值给nextCurrentHook
  • currentHook赋值为nextCurrentHook, 记录更新的数据来源,方便之后的hook,通过next连接起来。
  • 赋值workInProgressHook标记正在执行的hook

这里有一个难点,就是nextCurrentHook === null的时候,我们可以抛出错误。

hook在条件语句中报错

我们晓得hook是不能在条件语句中执行的。那是如何做到报错的呢?接下来我们根据上面的updateWorkProgressHook源码分析。假如,伪代码如下所示: 在mount阶段的时候,是3个hook,在执行setNum(100),update阶段4个。

const [num, setNum] = useState(99);
const [num2, setNum] = useState(101);
const [num3, setNum] = useState(102);
if(num === 100) {
 const [num4, setNum] = useState(103);
}

这里我们就会执行四次updateWorkProgressHook,我们来分析一下。

  • nextCurrentHook = currentHook = m-hook1,第一次后currentHook不为null
  • nextCurrentHook等于m-hook2
  • nextCurrentHook等于m-hook3
  • 第四次的时候nextCurrentHook = m-hook3.next = null, 所以就会走到报错的逻辑。

useState计算

上一部分我们已经知道了update的时候,hook的数据来源,我们现在得到数据了,那如何通过之前的数据,计算出新的数据呢?

  • 在执行setNum(action)后,我们知道action 存放在queue.shared.pending
  • queue是存放在对应hookupdateQueue中。所以我们可以拿到action
  • 第三步就是去消费action,即执行processUpdateQueue, 传入上一次的state, 以及我们这次接受的action,计算最新的值。
function updateState<State>(): [State, Dispatch<State>] {
  // 找到当前useState对应的hook数据
  const hook = updateWorkInProgressHook();
  // 计算新的state逻辑
  const queue = hook.updateQueue as UpdateQueue<State>;
  const pending = queue.shared.pending;
  if (pending !== null) {
    const { memoizedState } = processUpdateQueue(hook.memoizedState, pending);
    hook.memoizedState = memoizedState;
  }
  return [hook.memoizedState, queue.dispatch as Dispatch<State>];
}

这样,我们就在渲染的时候拿到了最新的值,以及重新返回的dispatch

双缓存树

在第一次更新的时候,我们的双缓存树还没有建立起来,在第一次更新之后,双缓存树就建立完成。

之后每一次调和生成子fiberNode的时候,都会利用alternate指针去重复利用相同type和相同key的节点。

例如初始化的时候num的值为3, 通过setNum(4)调用第一次更新后。首先会创建一个wip tree

在执行完commitWork后,屏幕上渲染为4后,root.current的指向会被修改 为wip tree

当我们再setNum(5)的时候,第二次更新后,双缓存树已经建立。会利用之前右边的4fiberNode tree,进行下一轮渲染。

总结

此节我们主要是讲了hook是如何存放数据的,以及mount阶段和update阶段不同的存放,也讲解了通过dispatch调用后,react是如何更新的。以及双缓存树在第一次更新后是如何建立的。

以上就是React18从0实现dispatch update流程的详细内容,更多关于React18 dispatch update流程的资料请关注我们其它相关文章!

(0)

相关推荐

  • React commit源码分析详解

    目录 总览 commitBeforeMutationEffects commitMutationEffects 插入 dom 节点 获取父节点及插入位置 判断当前节点是否为单节点 在对应位置插入节点 更新 dom 节点 更新 HostComponent 更新 HostText 删除 dom 节点 unmountHostComponents commitNestedUnmounts commitUnmount commitLayoutEffects 执行生命周期 处理回调 总结 总览 commit

  • react echarts tree树图搜索展开功能示例详解

    目录 前言 最终效果 版本信息 核心功能: 关键思路: 附上代码 数据data.js 功能: TreeUtils 总结: 前言 umi+antd-admin 框架中使用类组件+antd结合echarts完成树图数据展示和搜索展开功能 最终效果 版本信息 "antd": "3.24.2", "umi": "^2.7.7", "echarts": "^4.4.0", "echart

  • React18系列reconciler从0实现过程详解

    目录 引言 React-Dom包 createRoot hostConfig.ts React-reconciler包 createContainer() 函数 render() 调用 创建更新createUpdate 将更新推进队列enqueueUpdate 开始调用scheduleUpdateOnFiber wookLoop beginWork开始 updateHostRoot reconcileChildren reconcileSingleElement 例子 completeWork开

  • react native图片解析流程详解

    目录 正文 1. 构建输出的产物 2. js bundle分析 3. 图片source拼接 3.1 如果bundle放在服务器(本地开发) 3.2 bundle内置在app中(app下载bundle和assets后执行) 4. Image style的witdh和height没有声明会发生什么? 正文 我们知道,在react-native中加载一张图片需要使用Image组件,其有两种使用方式 import bg from './bg.png'; // 1. 加载本地图片资源 <Image sou

  • react hooks d3实现企查查股权穿透图结构图效果详解

    目录 前言 最终效果: 版本信息: 股权穿透图基础功能: 股权结构图基础功能: 股权穿透图代码 股权结构图代码 总结: 前言 umi+antd-admin 框架中使用hooks结合d3完成类似股权穿透图和股权结构图(web) 最终效果: 股权穿透图 股权结构图 版本信息: "d3": "4.13.0", "antd": "3.24.2", "umi": "^2.7.7", 股权穿透图基础

  • react-native只保留3x图原理解析

    目录 引言 1. 输出构建产物 2. RN如何决定加载哪张scale图片? 3. repo中是否可以只保留3x图? 3.1 资源上传 3.2 资源下载 4. 结论 引言 我们的react-native项目中,一张图片一般会存在1x, 1.5x, 2x, 3x几种尺寸(1.5x是android特有的),以便在不同屏幕尺寸的手机加载对应尺寸的图片. 1. 输出构建产物 如果我们在代码中引入了一张图片,例如 // index.js import bg from './bg.png'; . ├── in

  • React 状态管理工具优劣势示例分析

    目录 什么是状态管理? React 状态管理方案 方案介绍 方案对比 Source 相关建议 什么是状态管理? “状态”是描述应用程序当前行为的任何数据.这可能包括诸如“从服务器获取的对象列表”.“当前选择的项目”.“当前登录用户的名称”和“此模式是否打开?”等值. 众所周知,我们在研发一个复杂应用的过程中,一套好的状态管理方案是必不可少的,既能提升研发效率,又能降低研发维护成本,那么状态管理方案那么多,它们有什么不同,我们又该如何选择适合当前应用的方案呢? 本期将主要就 react 的常用状态

  • NPM 安装cordova时警告:npm WARN deprecated minimatch@2.0.10: Please update to minimatch 3.0.2 or higher to

    前言: NPM是随同NodeJS一起安装的包管理工具,能解决NodeJS代码部署上的很多问题,常见的使用场景有以下几种: 允许用户从NPM服务器下载别人编写的第三方包到本地使用. 允许用户从NPM服务器下载并安装别人编写的命令行程序到本地使用. 允许用户将自己编写的包或命令行程序上传到NPM服务器供别人使用. NPM 安装cordova时警告:npm WARN deprecated minimatch@2.0.10: Please update to minimatch 3.0.2 or hig

  • OAuth 2.0 概念及授权流程梳理

    OAuth2 的概念 OAuth是一个关于授权的开放网络标准,OAuth2是其2.0版本. 它规定了四种操作流程(授权模式)来确保安全 应用场景有第三方应用的接入.微服务鉴权互信.接入第三方平台.第一方密码登录等 Java王国中Spring Security也对OAuth2标准进行了实现. OAuth2授权模式 OAuth2定义了四种授权模式(授权流程)来对资源的访问进行控制 授权码模式(Authorization Code Grant) 隐式授权模式(Implicit Grant) 用户名密码

  • Vue3.0数据响应式原理详解

    基于Vue3.0发布在GitHub上的第一版源码(2019.10.05)整理 预备知识 ES6 Proxy,整个响应式系统的基础. 新的composition-API的基本使用,目前还没有中文文档,可以先通过这个仓库(composition-api-rfc)了解,里面也有对应的在线文档. 先把Vue3.0跑起来 先把vue-next仓库的代码clone下来,安装依赖然后构建一下,vue的package下的dist目录下找到构建的脚本,引入脚本即可. 下面一个简单计数器的DEMO: <!DOCTY

  • 采用React编写小程序的Remax框架的编译流程解析(推荐)

    Remax是蚂蚁开源的一个用React来开发小程序的框架,采用运行时无语法限制的方案.整体研究下来主要分为三大部分:运行时原理.模板渲染原理.编译流程:看了下现有大部分文章主要集中在Reamx的运行时和模板渲染原理上,而对整个React代码编译为小程序的流程介绍目前还没有看到,本文即是来补充这个空白. 关于模板渲染原理看这篇文章:https://www.jb51.net/article/132635.htm 关于remax运行时原理看这篇文章:https://www.jb51.net/artic

  • vue element后台鉴权流程分析

    前言: 最近项目遇到一个管理系统,感觉权限配置挺有意思,记录一下流程实现的过程,便于自己学习以及整理思路,部分思路整合在代码的注释中: 路由拦截鉴权常用的两种方法 1:路由拦截:单纯给路由加字段标识符,通过路由拦截实现 2:动态路由:第二种是通过路由的拆分另外需要后端的配合去实现的动态路由配置 比较: 路由拦截实现方式比较简单,只需要简单的在router.beforeEach中根据路由配置信息过滤页面是否有权限前往改组件,若相对于的权限不够则不前往相应的组件 动态路由实现相对比较复杂,并且需要后

  • python ansible自动化运维工具执行流程

    ansible 简介 ansible 是什么? ansible是新出现的自动化运维工具,基于Python开发,集合了众多运维工具(puppet.chef.func.fabric)的优点,实现了批量系统配置.批量程序部署.批量运行命令等功能. ansible是基于 paramiko 开发的,并且基于模块化工作,本身没有批量部署的能力.真正具有批量部署的是ansible所运行的模块,ansible只是提供一种框架.ansible不需要在远程主机上安装client/agents,因为它们是基于ssh来

  • Python实战项目刮刮乐的实现详解流程

    目录 导语 正文 1)环境安装 2)正式敲代码 2.1定义必要常量 2.2设置随机读取图片 2.3主程序 3)效果图展示 3.1 part 随机图一 3.2 part 随机图二 3.3 part 随机图三 总结 导语 在CSDN学习的过程中,遇到了爆火的文章是关于刮刮卡的! 大家猜猜看是谁写的? 我看这文章都特别火,我也感觉挺好玩的,那就寻思用 Python肯定也能做呀! 这不?今天还有时间,那就带大家写一款刮刮乐的小程序吧~ 正文 1)环境安装 准备好Python3.Pycharm.Pygam

  • JavaScript 精美贪吃蛇实现流程

    目录 一.创建html结构 二.创建表格 三.创建蛇头.蛇身 四.创建食物 五.让蛇动起来 六.控制蛇的方向 七.完整代码 index.html Game.js Snake.js Food.js 八.图片 九.总结 这篇文章不简单!! 今天博主呕心沥血写了一个贪吃蛇的小游戏,整个过程从无到有简直扣人心弦. 接下来本博主就来好好说到说到这个过程!! 话不多说,我们还是先来看看最后的呈现效果吧. 看完了才知道什么叫做操作,简直传奇!! 接下来不吹牛来讲讲实际操作 ! 首先我们要知道所谓的贪吃蛇无非就

  • Postgres中UPDATE更新语句源码分析

    目录 PG中UPDATE源码分析 整体流程分析 解析部分——生成语法解析树UpdateStmt 解析部分——生成查询树Query 优化器——生成执行计划 执行器 事务 总结 PG中UPDATE源码分析 本文主要描述SQL中UPDATE语句的源码分析,代码为PG13.3版本. 整体流程分析 以update dtea set id = 1;这条最简单的Update语句进行源码分析(dtea不是分区表,不考虑并行等,没有建立任何索引),帮助我们理解update的大致流程. SQL流程如下: parse

  • vue-router后台鉴权流程实现

    目录 前言: 路由拦截鉴权常用的两种方法 比较: 与动态路由相关的通常有以下几个文件: router.js permission.js store.js 退出登录: 结尾: 前言: 最近项目遇到一个管理系统,感觉权限配置挺有意思,记录一下流程实现的过程,便于自己学习以及整理思路,部分思路整合在代码的注释中: 路由拦截鉴权常用的两种方法 路由拦截:单纯给路由加字段标识符,通过路由拦截实现动态路由:第二种是通过路由的拆分另外需要后端的配合去实现的动态路由配置 比较: 路由拦截实现方式比较简单,只需要

随机推荐