通过本章第一节学习,我们知道状态更新
流程开始后首先会创建Update对象
。
本节我们学习Update
的结构与工作流程。
你可以将
Update
类比心智模型
中的一次commit
。
Update的分类
我们先来了解Update
的结构。
首先,我们将可以触发更新的方法所隶属的组件分类:
ReactDOM.render —— HostRoot
this.setState —— ClassComponent
this.forceUpdate —— ClassComponent
useState —— FunctionComponent
useReducer —— FunctionComponent
可以看到,一共三种组件(HostRoot
| ClassComponent
| FunctionComponent
)可以触发更新。
由于不同类型组件工作方式不同,所以存在两种不同结构的Update
,其中ClassComponent
与HostRoot
共用一套Update
结构,FunctionComponent
单独使用一种Update
结构。
虽然他们的结构不同,但是他们工作机制与工作流程大体相同。在本节我们介绍前一种Update
,FunctionComponent
对应的Update
在Hooks
章节介绍。
Update的结构
ClassComponent
与HostRoot
(即rootFiber.tag
对应类型)共用同一种Update结构
。
对应的结构如下:
const update: Update<*> = {
eventTime,
lane,
suspenseConfig,
tag: UpdateState,
payload: null,
callback: null,
next: null,
};
Update
由createUpdate
方法返回,你可以从这里 (opens new window)看到createUpdate
的源码
字段意义如下:
eventTime:任务时间,通过
performance.now()
获取的毫秒数。由于该字段在未来会重构,当前我们不需要理解他。lane:优先级相关字段。当前还不需要掌握他,只需要知道不同
Update
优先级可能是不同的。
你可以将
lane
类比心智模型
中需求的紧急程度
。
suspenseConfig:
Suspense
相关,暂不关注。tag:更新的类型,包括
UpdateState
|ReplaceState
|ForceUpdate
|CaptureUpdate
。payload:更新挂载的数据,不同类型组件挂载的数据不同。对于
ClassComponent
,payload
为this.setState
的第一个传参。对于HostRoot
,payload
为ReactDOM.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
。这其实是很常见的情况。
在这里介绍一种最简单的情况:
onClick() {
this.setState({
a: 1
})
this.setState({
b: 2
})
}
在一个ClassComponent
中触发this.onClick
方法,方法内部调用了两次this.setState
。这会在该fiber
中产生两个Update
。
Fiber节点
最多同时存在两个updateQueue
:
current fiber
保存的updateQueue
即current updateQueue
workInProgress fiber
保存的updateQueue
即workInProgress updateQueue
在commit阶段
完成页面渲染后,workInProgress Fiber树
变为current Fiber树
,workInProgress Fiber树
内Fiber节点
的updateQueue
就变成current updateQueue
。
updateQueue
updateQueue
有三种类型,其中针对HostComponent
的类型我们在completeWork一节介绍过。
剩下两种类型和Update
的两种类型对应。
ClassComponent
与HostRoot
使用的UpdateQueue
结构如下:
const queue: UpdateQueue<State> = {
baseState: fiber.memoizedState,
firstBaseUpdate: null,
lastBaseUpdate: null,
shared: {
pending: null,
},
effects: null,
};
UpdateQueue
由initializeUpdateQueue
方法返回,你可以从这里 (opens new window)看到initializeUpdateQueue
的源码
字段说明如下:
- baseState:本次更新前该
Fiber节点
的state
,Update
基于该state
计算更新后的state
。
你可以将
baseState
类比心智模型
中的master分支
。
firstBaseUpdate
与lastBaseUpdate
:本次更新前该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 !== null
的Update
。
例子
updateQueue
相关代码逻辑涉及到大量链表操作,比较难懂。在此我们举例对updateQueue
的工作流程讲解下。
假设有一个fiber
刚经历commit阶段
完成渲染。
该fiber
上有两个由于优先级过低所以在上次的render阶段
并没有处理的Update
。他们会成为下次更新的baseUpdate
。
我们称其为u1
和u2
,其中u1.next === u2
。
fiber.updateQueue.firstBaseUpdate === u1;
fiber.updateQueue.lastBaseUpdate === u2;
u1.next === u2;
我们用-->
表示链表的指向:
fiber.updateQueue.baseUpdate: u1 --> u2
现在我们在fiber
上触发两次状态更新,这会先后产生两个新的Update
,我们称为u3
和u4
。
每个 update
都会通过 enqueueUpdate
方法插入到 updateQueue
队列上
当插入u3
后:
fiber.updateQueue.shared.pending === u3;
u3.next === u3;
shared.pending
的环状链表,用图表示为:
fiber.updateQueue.shared.pending: u3 ─────┐
^ |
└──────┘
接着插入u4
之后:
fiber.updateQueue.shared.pending === u4;
u4.next === u3;
u3.next === u4;
shared.pending
是环状链表,用图表示为:
fiber.updateQueue.shared.pending: u4 ──> u3
^ |
└──────┘
shared.pending
会保证始终指向最后一个插入的update
,你可以在这里 (opens new window)看到enqueueUpdate
的源码
更新调度完成后进入render阶段
。
此时shared.pending
的环被剪开并连接在updateQueue.lastBaseUpdate
后面:
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
完成,你可以从这里 (opens new window)看到processUpdateQueue
的源码
state
的变化在render阶段
产生与上次更新不同的JSX
对象,通过Diff算法
产生effectTag
,在commit阶段
渲染在页面上。
渲染完成后workInProgress Fiber树
变为current Fiber树
,整个更新流程结束。