浅谈 React Update
3 views
在深入源码前,让我们先建立更新机制
的心智模型
。
1. 心智模型
1.1 同步更新的 React
我们可以将更新机制
类比代码版本控制
。
在没有代码版本控制
前,我们在代码中逐步叠加功能。一切看起来井然有序,直到我们遇到了一个紧急线上bug(红色节点)。
为了修复这个bug,我们需要首先将之前的代码提交。
在React
中,所有通过ReactDOM.render
创建的应用(其他创建应用的方式参考ReactDOM.render一节)都是通过类似的方式更新状态
。
即没有优先级
概念,高优更新
(红色节点)需要排在其他更新
后面执行。
1.2 并发更新的React
当有了代码版本控制
,有紧急线上bug需要修复时,我们暂存当前分支的修改,在master分支
修复bug并紧急上线。
bug修复上线后通过git rebase
命令和开发分支
连接上。开发分支
基于修复bug的版本
继续开发。
在React
中,通过ReactDOM.createBlockingRoot
和ReactDOM.createRoot
创建的应用会采用并发
的方式更新状态
。
高优更新
(红色节点)中断正在进行中的低优更新
(蓝色节点),先完成render - commit流程
。
待高优更新
完成后,低优更新
基于高优更新
的结果重新更新
。
2.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
章节介绍。
3. Update的结构
ClassComponent
与 HostRoot
(即rootFiber.tag
对应类型)共用同一种 Update结构
。
对应的结构如下:
const update: Update<*> = {
eventTime,
lane,
suspenseConfig,
tag: UpdateState,
payload: null,
callback: null,
next: null,
};
你可以将Update
类比心智模型
中的一次commit
。
Update
由createUpdate
方法返回,你可以从这里看到createUpdate
的源码
字段意义如下:
- eventTime:任务时间,通过
performance.now()
获取的毫秒数。由于该字段在未来会重构,当前我们不需要理解他。 - lane:优先级相关字段。当前还不需要掌握他,只需要知道不同
Update
优先级可能是不同的。 - suspenseConfig:
Suspense
相关,暂不关注。 - tag:更新的类型,包括
UpdateState
|ReplaceState
|ForceUpdate
|CaptureUpdate
。 - payload:更新挂载的数据,不同类型组件挂载的数据不同。对于
ClassComponent
,payload
为this.setState
的第一个传参。对于HostRoot
,payload
为ReactDOM.render
的第一个传参。 - callback:更新的回调函数。
- next:与其他
Update
连接形成链表。
4. 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
。
5. updateQueue
updateQueue
有三种类型,其中针对HostComponent
的类型我们在completeWork一节介绍过。
剩下两种类型和Update
的两种类型对应。
ClassComponent
与HostRoot
使用的UpdateQueue
结构如下:
const queue: UpdateQueue<State> = {
baseState: fiber.memoizedState,
firstBaseUpdate: null,
lastBaseUpdate: null,
shared: {
pending: null,
},
effects: null,
};
字段说明如下:
- 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
。
在遍历时如果有优先级低的Update
会被跳过。
当遍历完成后获得的state
,就是该Fiber节点
在本次更新的state
(源码中叫做memoizedState
)。
render阶段
的Update操作
由processUpdateQueue
完成,你可以从这里看到processUpdateQueue
的源码
state
的变化在render阶段
产生与上次更新不同的JSX
对象,通过Diff算法
产生effectTag
,在commit阶段
渲染在页面上。
渲染完成后workInProgress Fiber树
变为current Fiber树
,整个更新流程结束。