React 中的 UpdateQueue 详解

Update

面试题:说一说 React 中的 updateQueue

在 React 中,有许多触发状态更新的方法,比如:

  • ReactDOM.createRoot
  • this.setState
  • this.forceUpdate
  • useState dispatcher
  • useReducer dispatcher

虽然这些方法执行的场景会有所不同,但是都可以接入同样的更新流程,原因是因为它们使用同一种数据结构来表示更新,这种数据结构就是 Update。

Update 数据结构

在 React 中,更新实际上是存在优先级的,其心智模型有一些类似于"代码版本管理工具"。

举个例子,假设现在我们在开发一个软件,当前软件处于正常的迭代中,拥有 A、B、C 三个正常需求,此时突然来了一个紧急的线上 Bug,整体流程如下:

为了修复线上 Bug D,你需要先完成需求 A、B、C,之后才能进行 D 的修复,这样的设计实际上是不合理的。

有了代码版本管理工具之后,有紧急线上 Bug 需要修复时,可以先暂存当前分支的修改,在 master 分支修复 Bug D 并紧急上线:

当 Bug 修复完毕后,再正常的来迭代 A、B、C 需求,之后的迭代会基于 D 这个版本:

并发更新的 React 也拥有相似的能力,不同的 update 是有不同的优先级,高优先级的 update 能够中断低优先级的 update,当高优先级的 update 完成更新之后,后续的低优先级更新会在高优先级 update 更新后的 state 的基础上再来进行更新。

接下来我们来看一下 Update 的一个数据结构。

在前面我们说了在 React 中,有不同的触发更新的方法,不同的方法实际上对应了不同的组件:

  • ReactDOM.createRoot 对应 HostRoot
  • this.setState 对应 ClassComponent
  • this.forceUpdate 对应 ClassComponent
  • useState dispatcher 对应 FunctionComponent
  • useReducer dispatcher 对应 FunctionComponent

不同的组件类型,所对应的 Update 的数据结构是不同的。

HostRoot 和 ClassComponent 这一类组件所对应的 Update 数据结构如下:

jsx 复制代码
function createUpdate(eventTime, lane){
  const update = {
    eventTime,
    lane,
        // 区分触发更新的场景
    tag: UpdateState,
    payload: null,
    // UI 渲染后触发的回调函数
    callback: null,
    next: null,
  };
  return update;
}

在上面的 Update 数据结构中,tag 字段是用于区分触发更新的场景的,选项包括:

  • ReplaceState:代表在 ClassComponent 生命周期函数中直接改变 this.state
  • UpdateState:默认情况,通过 ReactDOM.createRoot 或者 this.setState 触发更新
  • CaptureUpdate:代表发生错误的情况下在 ClassComponent 或 HostRoot 中触发更新(比如通过 getDerivedStateFormError 方法)
  • ForceUpdate:代表通过 this.forceUpdate 触发更新

接下来来看一下 FunctionComponent 所对应的 Update 数据结构:

jsx 复制代码
const update = {
  lane,
  action,
  // 优化策略相关字段
  hasEagerState: false,
  eagerState: null,
  next: null
}

在上面的数据结构中,有 hasEagerState 和 eagerState 这两个字段,它们和后面要介绍的 React 内部的性能优化策略(eagerState 策略)相关。

在 Update 数据结构中,有三个问题是需要考虑:

  • 更新所承载的更新内容是什么

对于HostRoot以及类组件来讲,承载更新内容的字段为 payload 字段

jsx 复制代码
// HostRoot
ReactDOM.createRoot(rootEle).render(<App/>);                                   
// 对应 update
{
    payload : {
    // HostRoot 对应的 jsx,也就是 <App/> 对应的 jsx
    element                                  
  },
  // 省略其他字段
}
​
// ClassComponent 情况1
this.setState({num : 1})
// 对应 update
{
  payload : {
    num: 1
  },
  // 省略其他字段
}
​
// ClassComponent 情况2
this.setState({num : num => num + 1})
// 对应 update
{
  payload : {
    num: num => num + 1
  },
  // 省略其他字段
}

对于函数组件来讲,承载更新内容的字段为 action 字段

jsx 复制代码
// FC 使用 useState 情况1
updateNum(1);
// 对应 update
{
  action : 1,
  // 省略其他字段
}
​
// FC 使用 useState 情况2
updateNum(num => num + 1);
// 对应 update
{
  action : num => num + 1,
  // 省略其他字段
}
  • 更新的紧急程度:紧急程度是由 lane 字段来表示的
  • 更新之间的顺序:通过 next 字段来指向下一个 update,从而形成一个链表。

UpdateQueue

上面所介绍的 update 是计算 state 的最小单位,updateQueue 是由 update 组成的一个链表,updateQueue 的数据结构如下:

jsx 复制代码
const updateQueue = {
  baseState: null,
  firstBaseUpdate: null,
  lastBaseUpdate: null,
  shared: {
    pending: null
  }
}
  • baseState:参与计算的初始 state,update 基于该 state 计算新的 state,可以类比为心智模型中的 master 分支。
  • firstBaseUpdate 与 lastBaseUpdate:表示更新前该 FiberNode 中已保存的 update,以链表的形式串联起来。链表头部为 firstBaseUpdate,链表尾部为 lastBaseUpdate。
  • shared.pending:触发更新后,产生的 update 会保存在 shared.pending 中形成单向环状链表。计算 state 时,该环状链表会被拆分并拼接在 lastBaseUpdate 后面。

举例说明,例如当前有一个 FiberNode 刚经历完 commit 阶段的渲染,该 FiberNode 上面有两个"由于优先级低,导致在上一轮 render 阶段并没有被处理的 update",假设这两个 update 分别名为 u0 和 u1

jsx 复制代码
fiber.updateQueue.firstBaseUpdate = u0;
fiber.updateQueue.lastBaseUpdate = u1;
u0.next = u1;

那么假设在当前的 FiberNode 上面我们又触发了两次更新,分别产生了两个 update(u2 和 u3),新产生的 update 就会形成一个环状链表,shared.pending 就会指向这个环状链表,如下图所示:

之后进入新的一轮 render,在该 FiberNode 的 beginWork 中,shared.pending 所指向的环状链表就会被拆分,拆分之后接入到 baseUpdate 链表后面:

接下来就会遍历 updateQueue.baseUpdate,基于 updateQueue.baseState 来计算每个符合优先级条件的 update(这个过程有点类似于 Array.prototype.reduce),最终计算出最新的 state,该 state 被称之为 memoizedState。

因此我们总结一下,整个 state 的计算流程可以分为两步:

  • 将 shared.pending 所指向的环状链表进行拆分并且和 baseUpdate 进行拼接,形成新的链表
  • 遍历连接后的链表,根据 wipRootRenderLanes 来选定优先级,基于符合优先级条件的 update 来计算 state

真题解答

题目:面试题:说一说 React 中的 updateQueue

参考答案:

update 是计算 state 的最小单位,一条 updateQueue 代表由 update 所组成的链表,其中几个重要的属性列举如下:

  • baseState:参与计算的初始 state,update 基于该 state 计算新的 state,可以类比为心智模型中的 master 分支。
  • firstBaseUpdate 与 lastBaseUpdate:表示更新前该 FiberNode 中已保存的 update,以链表的形式串联起来。链表头部为 firstBaseUpdate,链表尾部为 lastBaseUpdate。
  • shared.pending:触发更新后,产生的 update 会保存在 shared.pending 中形成单向环状链表。计算 state 时,该环状链表会被拆分并拼接在 lastBaseUpdate 后面。

整个 state 的计算流程可以分为两步:

  • 将 shared.pending 所指向的环状链表进行拆分并且和 baseUpdate 进行拼接,形成新的链表
  • 遍历连接后的链表,根据 wipRootRenderLanes 来选定优先级,基于符合优先级条件的 update 来计算 state

以下是简易版理解方式

想象一下,你有一个玩具盒子,里面有好多小玩具。这些小玩具就像是 React 里的"更新"(Update),它们会告诉 React 怎么去改变一个组件(就像一个玩具)的状态。这些更新会被放在一个队列(UpdateQueue)里,就像排队等着被处理的小玩具。

UpdateQueue 是什么?

UpdateQueue 就是一个用来管理这些更新的"盒子"。它有几部分:

  • baseState:这是最开始的状态,就像你刚拿到玩具时的样子。
  • firstBaseUpdate 和 lastBaseUpdate:这是已经排队等着的更新,就像已经排好队的小朋友。
  • shared.pending:这是新来的更新,它们会先围成一个圈(环状链表),等轮到它们的时候再加入队列。

更新是怎么工作的?

当 React 要更新一个组件时,它会做两件事:

  1. 把新来的更新(shared.pending)从圈圈里拿出来,加到已经排队的更新后面。
  2. 然后,React 会按照优先级(比如紧急的先处理)一个个看这些更新,用它们来改变组件的状态。

举个例子

假设你有一个积木,最开始是红色的(baseState)。然后你决定把它涂成蓝色(第一个更新),接着又想在上面画个小星星(第二个更新)。这些更新都会被放进 UpdateQueue 里。React 会按照顺序处理这些更新,最后积木就变成了你想要的样子。

React 源代码里的 UpdateQueue

在 React 的代码里,UpdateQueue 是用一个对象表示的,就像一个盒子。这个盒子有几部分,比如 baseStatefirstBaseUpdatelastBaseUpdateshared.pending。每次你调用 setState 或者其他更新方法时,React 就会往这个盒子里放一个新的更新。

jsx 复制代码
const updateQueue = {
  baseState: null, // 最开始的状态
  firstBaseUpdate: null, // 第一个更新
  lastBaseUpdate: null, // 最后一个更新
  shared: {
    pending: null // 新来的更新
  }
};

所以,UpdateQueue 就是一个用来管理更新的工具,它帮助 React 按顺序处理这些更新,让组件的状态能够正确地改变。就像你排队等玩具一样,React 也会按照顺序处理这些更新。

相关推荐
文刀竹肃1 天前
DVWA -SQL Injection-通关教程-完结
前端·数据库·sql·安全·网络安全·oracle
LYFlied1 天前
【每日算法】LeetCode 84. 柱状图中最大的矩形
前端·算法·leetcode·面试·职场和发展
Bigger1 天前
Tauri(21)——窗口缩放后的”失焦惊魂”,游戏控制权丢失了
前端·macos·app
Bigger1 天前
Tauri (20)——为什么 NSPanel 窗口不能用官方 API 全屏?
前端·macos·app
bug总结1 天前
前端开发中为什么要使用 URL().origin 提取接口根地址
开发语言·前端·javascript·vue.js·html
一招定胜负1 天前
网络爬虫(第三部)
前端·javascript·爬虫
Shaneyxs1 天前
从 0 到 1 实现CloudBase云开发 + 低代码全栈开发活动管理小程序(13)
前端
半山烟雨半山青1 天前
微信内容emoji表情包编辑器 + vue3 + ts + WrchatEmogi Editor
前端·javascript·vue.js
码途潇潇1 天前
Vue 事件机制全面解析:原生事件、自定义事件与 DOM 冒泡完全讲透
前端·javascript
zmirror1 天前
Monorepo 在 Docker 中的构建方案方案
前端