前两篇文章分别简要说明了render阶段和commit阶段的流程,那么什么会触发render呢?显然是组件中的状态更新,包含以下方法:
-
ReactDOM.render
-
this.setState
-
this.forceUpdate
-
useState
-
useReducer
这些方法调用的场景各不相同,他们是如何接入同一套状态更新机制呢?
1. 概览
1.1 创建Update对象
上面的问题的答案是:每次状态更新都会创建一个保存更新状态相关内容的对象,我们叫他Update
,这个对象保存在触发状态更新的fiber中。在render阶段的beginWork
中会根据Update
计算新的state。
1.2 从触发更新的fiber到root
我们知道,beginWork
是从rootFiber
开始向下遍历。那么是如何从触发状态更新的fiber回到rootFiber呢?
答案是:调用markUpdateLaneFromFiberToRoot
方法。
该方法做的工作可以概括为:从触发状态更新的fiber一直向上遍历到rootFiber,并返回rootFiber。
由于不同更新优先级不尽相同,所以过程中还会更新遍历到的fiber的优先级。这目前属于超纲内容。
1.2 调度更新
上一步完成后我们找到了rootFiber
,该rootFiber
对应的Fiber树中某个Fiber节点包含一个Update
。
接下来通知Scheduler
根据更新的优先级,决定以同步还是异步的方式调度本次更新。
这里调用的方法是ensureRootIsScheduled
。
if (newCallbackPriority === SyncLanePriority) {
// 任务已经过期,需要同步执行render阶段
newCallbackNode = scheduleSyncCallback(
performSyncWorkOnRoot.bind(null, root);
);
} else {
// 根据任务优先级异步执行render阶段
var schedulerPriorityLevel = lanePriorityToSchedulerPriority(
newCallbackPriority
);
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root);
);
}
留意这段代码可以发现这两句:
performSyncWorkOnRoot.bind(null, root);
performConcurrentWorkOnRoot.bind(null, root);
看起来很熟悉吧,因为这就是render环节的入口函数。这样一来,我们就知道状态更新是怎么触发render的了。
2. Update
2.1 分类
首先,我们将可以触发更新的方法所隶属的组件分类:
-
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章节介绍。
2.2 结构
第一种Update的结构如下:
const update: Update<*> = {
eventTime,
lane,
suspenseConfig,
tag: UpdateState,
payload: null,
callback: null,
next: null,
};
Update由
createUpdate
方法返回
字段意义如下:
-
eventTime:任务时间,通过performance.now()获取的毫秒数。由于该字段在未来会重构,当前我们不需要理解他。
-
lane:优先级相关字段。当前还不需要掌握他,只需要知道不同Update优先级可能是不同的。
-
suspenseConfig:Suspense相关,暂不关注。
-
tag:更新的类型,包括
UpdateState | ReplaceState | ForceUpdate | CaptureUpdate
。 -
payload:更新挂载的数据,不同类型组件挂载的数据不同。对于ClassComponent,payload为
this.setState
的第一个传参。对于HostRoot,payload为ReactDOM.render
的第一个传参。 -
callback:更新的回调函数。即在commit阶段的 layout 子阶段一节中提到的回调函数。
next:与其他Update连接形成链表。
2.3 和Fiber的联系
从参数中我们能留意到Update存在一个连接其他Update形成链表的字段next
。联系React中另一种以链表形式组成的结构Fiber,他们存在一定的关联。
类似Fiber节点组成Fiber树,Fiber节点上的多个Update会组成链表并被包含在fiber.updateQueue
中。
举个例子:
onClick() {
this.setState({
a: 1
})
this.setState({
b: 2
})
}
在一个ClassComponent
中触发onClick
方法,方法内部调用了两次this.setState
。这会在该fiber中产生两个Update。
Fiber节点最多同时存在两个updateQueue:
- current fiber保存的current updateQueue
- workInProgress fiber保存的workInProgress updateQueue
3. Update Queue
3.1 结构
ClassComponent
和HostRoot
使用的UpdateQueue结构如下:
const queue: UpdateQueue<State> = {
baseState: fiber.memorizedState,
firstBaseUpdate: null,
lastBaseUpdate: null,
shared: {
pending: null,
},
effects: null,
};
字段说明如下:
-
baseState:本次更新前该Fiber节点的state,Update基于该state计算更新后的state。
-
firstBaseUpdate / lastBaseUpdate:本次更新前该Fiber节点已保存的Update。以链表形式存在,链表头为firstBaseUpdate,链表尾为lastBaseUpdate。
之所以在更新产生前该Fiber节点内就存在Update,是由于某些Update优先级较低所以在上次render阶段由Update计算state时被跳过。 -
shared.pending:触发更新时,产生的Update会保存在shared.pending中形成单向环状链表。当由Update计算state时这个环会被剪开并连接在lastBaseUpdate后面。
-
effects:数组。保存
update.callback !== null
的Update。
3.2 整体流程
来举一个例子,看看整体的状态更新到render的流程是怎么连接起来的。
假设有一个fiber刚经历commit阶段完成渲染。
该fiber上有两个由于优先级过低所以在上次的render阶段并没有处理的Update。他们会成为下次更新的baseUpdate。
我们称其为u1和u2,其中u1.next === u2。
fiber.updateQueue.firstBaseUpdate === u1;
fiber.updateQueue.lastBaseUpdate === u2;
u1.next === u2;
现在我们在fiber上触发两次状态更新,这会先后产生两个新的Update,我们称为u3和u4。
每个 update 都会通过 enqueueUpdate
方法插入到 updateQueue
队列上。
fiber.updateQueue.shared.pending: u4 ──> u3
^ |
└──────┘
更新调度完成后进入render阶段。
此时shared.pending的环被剪开并连接在updateQueue.lastBaseUpdate后面:
fiber.updateQueue.baseUpdate: u1 --> u2 --> u3 --> u4
接下来遍历updateQueue.baseUpdate链表,以baseState为初始state,依次与遍历到的每个Update计算并产生新的state(该操作类似于数组的reduce方法)。
在遍历时如果有优先级低的Update会被跳过。
当遍历完成后获得的state,就是该Fiber节点在本次更新的state(源码中叫做memorizedState
)。
render阶段的Update操作由processUpdateQueue
完成
state的变化在render阶段产生与上次更新不同的JSX对象,通过Diff算法产生effectTag
,在commit阶段渲染在页面上。
dom节点渲染完成后(commit中的mutation阶段结束后)workInProgress Fiber树变为current Fiber树,整个更新流程结束。
来看最终的流程图:
创建fiberRootNode、rootFiber、updateQueue(`legacyCreateRootFromDOMContainer`)
|
|
v
创建Update对象(`updateContainer`)
|
|
v
从fiber到root(`markUpdateLaneFromFiberToRoot`)
|
|
v
调度更新(`ensureRootIsScheduled`)
|
|
v
render阶段(`performSyncWorkOnRoot` 或 `performConcurrentWorkOnRoot`)
|
|
v
commit阶段(`commitRoot`)