通过本章第一节学习,我们知道状态更新流程开始后首先会创建Update对象

本节我们学习Update的结构与工作流程。

你可以将Update类比心智模型中的一次commit

Update的分类

我们先来了解Update的结构。

首先,我们将可以触发更新的方法所隶属的组件分类:

  • ReactDOM.render —— HostRoot

  • this.setState —— ClassComponent

  • this.forceUpdate —— ClassComponent

  • useState —— FunctionComponent

  • useReducer —— FunctionComponent

可以看到,一共三种组件(HostRoot | ClassComponent | FunctionComponent)可以触发更新。

由于不同类型组件工作方式不同,所以存在两种不同结构的Update,其中ClassComponentHostRoot共用一套Update结构,FunctionComponent单独使用一种Update结构。

虽然他们的结构不同,但是他们工作机制与工作流程大体相同。在本节我们介绍前一种UpdateFunctionComponent对应的UpdateHooks章节介绍。

Update的结构

ClassComponentHostRoot(即rootFiber.tag对应类型)共用同一种Update结构

对应的结构如下:

  1. const update: Update<*> = {
  2. eventTime,
  3. lane,
  4. suspenseConfig,
  5. tag: UpdateState,
  6. payload: null,
  7. callback: null,
  8. next: null,
  9. };

UpdatecreateUpdate方法返回,你可以从这里Update - 图1 (opens new window)看到createUpdate的源码

字段意义如下:

  • eventTime:任务时间,通过performance.now()获取的毫秒数。由于该字段在未来会重构,当前我们不需要理解他。

  • lane:优先级相关字段。当前还不需要掌握他,只需要知道不同Update优先级可能是不同的。

你可以将lane类比心智模型需求的紧急程度

  • suspenseConfig:Suspense相关,暂不关注。

  • tag:更新的类型,包括UpdateState | ReplaceState | ForceUpdate | CaptureUpdate

  • payload:更新挂载的数据,不同类型组件挂载的数据不同。对于ClassComponentpayloadthis.setState的第一个传参。对于HostRootpayloadReactDOM.render的第一个传参。

  • callback:更新的回调函数。即在commit 阶段的 layout 子阶段一节中提到的回调函数

  • next:与其他Update连接形成链表。

Update与Fiber的联系

我们发现,Update存在一个连接其他Update形成链表的字段next。联系React中另一种以链表形式组成的结构Fiber,他们之间有什么关联么?

答案是肯定的。

双缓存机制一节我们知道,Fiber节点组成Fiber树,页面中最多同时存在两棵Fiber树

  • 代表当前页面状态的current Fiber树

  • 代表正在render阶段workInProgress Fiber树

类似Fiber节点组成Fiber树Fiber节点上的多个Update会组成链表并被包含在fiber.updateQueue中。

什么情况下一个Fiber节点会存在多个Update?

你可能疑惑为什么一个Fiber节点会存在多个Update。这其实是很常见的情况。

在这里介绍一种最简单的情况:

  1. onClick() {
  2. this.setState({
  3. a: 1
  4. })
  5. this.setState({
  6. b: 2
  7. })
  8. }

在一个ClassComponent中触发this.onClick方法,方法内部调用了两次this.setState。这会在该fiber中产生两个Update

Fiber节点最多同时存在两个updateQueue

  • current fiber保存的updateQueuecurrent updateQueue

  • workInProgress fiber保存的updateQueueworkInProgress updateQueue

commit阶段完成页面渲染后,workInProgress Fiber树变为current Fiber树workInProgress Fiber树Fiber节点updateQueue就变成current updateQueue

updateQueue

updateQueue有三种类型,其中针对HostComponent的类型我们在completeWork一节介绍过。

剩下两种类型和Update的两种类型对应。

ClassComponentHostRoot使用的UpdateQueue结构如下:

  1. const queue: UpdateQueue<State> = {
  2. baseState: fiber.memoizedState,
  3. firstBaseUpdate: null,
  4. lastBaseUpdate: null,
  5. shared: {
  6. pending: null,
  7. },
  8. effects: null,
  9. };

UpdateQueueinitializeUpdateQueue方法返回,你可以从这里Update - 图2 (opens new window)看到initializeUpdateQueue的源码

字段说明如下:

  • baseState:本次更新前该Fiber节点stateUpdate基于该state计算更新后的state

你可以将baseState类比心智模型中的master分支

  • firstBaseUpdatelastBaseUpdate:本次更新前该Fiber节点已保存的Update。以链表形式存在,链表头为firstBaseUpdate,链表尾为lastBaseUpdate。之所以在更新产生前该Fiber节点内就存在Update,是由于某些Update优先级较低所以在上次render阶段Update计算state时被跳过。

你可以将baseUpdate类比心智模型中执行git rebase基于的commit(节点D)。

  • shared.pending:触发更新时,产生的Update会保存在shared.pending中形成单向环状链表。当由Update计算state时这个环会被剪开并连接在lastBaseUpdate后面。

你可以将shared.pending类比心智模型中本次需要提交的commit(节点ABC)。

  • effects:数组。保存update.callback !== nullUpdate

例子

updateQueue相关代码逻辑涉及到大量链表操作,比较难懂。在此我们举例对updateQueue的工作流程讲解下。

假设有一个fiber刚经历commit阶段完成渲染。

fiber上有两个由于优先级过低所以在上次的render阶段并没有处理的Update。他们会成为下次更新的baseUpdate

我们称其为u1u2,其中u1.next === u2

  1. fiber.updateQueue.firstBaseUpdate === u1;
  2. fiber.updateQueue.lastBaseUpdate === u2;
  3. u1.next === u2;

我们用-->表示链表的指向:

  1. fiber.updateQueue.baseUpdate: u1 --> u2

现在我们在fiber上触发两次状态更新,这会先后产生两个新的Update,我们称为u3u4

每个 update 都会通过 enqueueUpdate 方法插入到 updateQueue 队列上

当插入u3后:

  1. fiber.updateQueue.shared.pending === u3;
  2. u3.next === u3;

shared.pending的环状链表,用图表示为:

  1. fiber.updateQueue.shared.pending: u3 ─────┐
  2. ^ |
  3. └──────┘

接着插入u4之后:

  1. fiber.updateQueue.shared.pending === u4;
  2. u4.next === u3;
  3. u3.next === u4;

shared.pending是环状链表,用图表示为:

  1. fiber.updateQueue.shared.pending: u4 ──> u3
  2. ^ |
  3. └──────┘

shared.pending 会保证始终指向最后一个插入的update,你可以在这里Update - 图3 (opens new window)看到enqueueUpdate的源码

更新调度完成后进入render阶段

此时shared.pending的环被剪开并连接在updateQueue.lastBaseUpdate后面:

  1. fiber.updateQueue.baseUpdate: u1 --> u2 --> u3 --> u4

接下来遍历updateQueue.baseUpdate链表,以fiber.updateQueue.baseState初始state,依次与遍历到的每个Update计算并产生新的state(该操作类比Array.prototype.reduce)。

在遍历时如果有优先级低的Update会被跳过。

当遍历完成后获得的state,就是该Fiber节点在本次更新的state(源码中叫做memoizedState)。

render阶段Update操作processUpdateQueue完成,你可以从这里Update - 图4 (opens new window)看到processUpdateQueue的源码

state的变化在render阶段产生与上次更新不同的JSX对象,通过Diff算法产生effectTag,在commit阶段渲染在页面上。

渲染完成后workInProgress Fiber树变为current Fiber树,整个更新流程结束。