前言
React是一个用于构建界面的JavaScript库。它的核心是跟踪组件状态变化并将更新后的状态更新到屏幕上。在React中,我们把这个过程称为 reconciliation (协调)。通过调用setState方法,React检查状态或属性是否已更改,并在UI层上更新。
背景介绍
首先看一个简单的例子:
class ClickCounter extends React.Component {
constructor(props) {
super(props);
this.state = {count: 0};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState((state) => {
return {count: state.count + 1};
});
}
render() {
return [
,
{this.state.count}
]
}
}
这是一个简单的计数器的例子, 点击按钮,组件的状态就会在处理程序中更新,组件的状态的更新反过来会引起span元素的内容更新。
下面是在协调阶段,React内部会有各种活动。以下是计数器在协调阶段所做的操作:
- 更新state的count属性
- 检查并比较ClickCounter的子代以及props
- 更新span元素
协调期间还会执行其他活动,例如调用生命周期方法,更新ref。这些活动在Fiber架构中统称为"work"(工作)。work的类型取决于React元素的类型。React元素有多种类型,比如类组件,函数组件,Portals,DOM节点等。而React元素类型则是由React.createElement的第一个参数决定。React.createElement函数在render创建元素中调用。
React.createElement到Fiber
JSX编译
{this.state.count}
JSX在经过编译后,会得到如下的结果。这是render方法真正返回的结果
class ClickCounter {
...
render() {
return [
React.createElement(
'button',
{
key: '1',
onClick: this.onClick
},
'Update counter'
),
React.createElement(
'span',
{
key: '2'
},
this.state.count
)
]
}
}
React.createElement函数会返回如下的结果:
[
{
$$typeof: Symbol(react.element),
type: 'button',
key: "1",
props: {
children: 'Update counter',
onClick: () => { ... }
}
},
{
$$typeof: Symbol(react.element),
type: 'span',
key: "2",
props: {
children: 0
}
}
]
- $$typeof属性,用于将它们标示为react元素
- type,key,props,用于描述元素
而对于组件
的元素,它没有props和key:
{
$$typeof: Symbol(react.element),
key: null,
props: {},
ref: null,
type: ClickCounter
}
Fiber节点
在协调期间,render方法返回的React元素会被合并到Fiber节点树之中。每一个React元素都有对应的Fiber节点。与React元素不同,Fiber不会在每一次渲染的时候重新创建。Fiber会保存组件的状态和DOM。
前面讨论过,根据不同的React元素类型,会执行不同的活动。例如,对于Class组件会调用生命周期方法以及render方法。而对于DOM节点,它执行DOM mutation。因此,每个React元素都被转换为相应类型的Fiber节点。节点描述了需要完成的"work"。
可以将Fiber节点看作一种数据解构,表示一个work单元。,Fiber架构还提供了一种跟踪、调度、暂停和中止work的方法。
React会在首次将React元素转换为Fiber节点时,使用createFiberFromTypeAndProps
函数创建Fiber节点。在更新阶段会复用Fiber节点,并使用React元素上的数据更新Fiber节点上的属性。亦或者移动,删除Fiber节点。
React源码中的ChildReconciler函数包含了Fiber节点中所有的work
React会为每一个React元素创建一个Fiber节点,我们会得到一个Fiber节点树
Fiber节点树是通过链表的形式存储的,每一个Fiber都拥有child(第一个子节点的引用),sibling(第一个兄弟节点的引用)和return(父节点的引用),来表示层级关心。更多内容请参考这篇文章React Fiber为什么使用链表来设计组件树
current tree 和 progress tree
在第一次渲染完成后,React会生成一个Fiber树。该树映射了应用程序的状态,这颗树被称为current tree
。当应用程序开始更新时,React会构建一个workInProgress tree
, workInProgress tree
映射了未来的状态。
所有的"work"都是在workInProgress tree
上的Fiber节点上进行的。当React开始遍历current tree
时,它会为每一个现有的Fiber节点创建一个备份(alternate字段),alternate节点构成了workInProgress tree
。当所有更新和相关的"work"完成。workInProgress tree
会刷新到屏幕上。workInProgress tree
此时变为了current tree
。
React的核心原则之一是"一致性", 它总是一次性更新DOM, 不会显示部分结果. workInProgress就是一个用户不可见的"草稿", React在它上面处理所有组件, 处理完成后将它再刷新到界面上.
在React的源码中,有很多从workInProgress tree
和current tree
中获取Fiber节点的函数。比如下面这个函数签名
function updateHostComponent(current, workInProgress, renderExpirationTime) {
...
}
workInProgress tree
的Fiber节点拥有current tree
对应节点的引用。反之亦然。
副作用
我们可以将React组件视为使用state和props计算UI的函数。其他的活动,比如手动修改DOM,调用生命周期都应该被视作一种副作用。在React的文档中也提及了这一点
你之前可能已经在 React 组件中执行过数据获取、订阅或者手动修改过 DOM。我们统一把这些操作称为“副作用”(side-effects),或者简称为“作用”(effects)。因为它们会影响其他组件,并且在渲染期间无法完成。
大多数state和props的更新都会导致副作用。应用effects是一种work类型。因此Fiber节点是一种跟踪更新和effects的便捷机制,每一个Fiber节点都有与之相关联的effects。它们被编码在effectTag字段之中。
Fiber中的effects定义了处理更新之后需要做的"work"。对于DOM元素,"work"包含了添加,更新,删除。对于类组件,包括了更新ref,调用componentDidMount和componentDidUpdate生命周期方法。还有其他effects对应于其他类型的Fibber。
Effects list
React处理更新非常快。为了达到更好的性能水平,采用了一些有趣的技术。其中之一就是将具有effects的Fiber节点,构建为线性列表,以方便快速迭代。迭代线性列表要比迭代树快的多,因为不需要迭代没有side-effects的节点。
effects list的目的是是标记出具有DOM更新,或其它与之关联的其他effects的节点。effects list是finishedWork树的子集。在workInProgress tree
和current tree
中使用nextEffect属性链接在一起。
丹·阿布拉莫夫(Dan Abramov)将Effects list提供了一个比喻。将Fiber想象成一颗圣诞树,用圣诞灯将所有有效的节点连接在一起。
为了可视化这一点,让我们想象下面的Fiber树,其中高亮显示的节点有一些“work”要做。
例如,我们的更新导致将c2插入到DOM中,d2和c1更改属性,b2触发生命周期方法。 Effects list列表将把它们链接在一起,这样React就可以遍历时跳过其他节点。
可以看到具有effects的节点如何链接在一起。当遍历节点时,React使用firstEffect指针确定列表的起始位置。上图的Effects list可以用下图表示
Fiber节点树的根
React应用都有一个或者多个充当容器的DOM元素
const domContainer = document.querySelector('#container');
ReactDOM.render(React.createElement(ClickCounter), domContainer);
React会为容器创建FiberRoot对象,可以使用容器的DOM引用访问Fiber root对象:
// Fiber root对象
const fiberRoot = query('#container')._reactRootContainer._internalRoot
Fiber root是React保留对Fiber树引用的地方,Fiber树存储在Fiber root对象的current属性中
// Fiber树
const hostRootFiberNode = fiberRoot.current
Fiber树的第一个节点是一种特殊的类型节点,叫做HostRoot。它在内部创建,是最顶层组件的父组件。通过HostRoot节点的stateNode属性可以访问FiberRoot节点.
// Fiber root对象
const fiberRoot = query('#container')._reactRootContainer._internalRoot
// hostRoot
const hostRootFiberNode = fiberRoot.current
// true
hostRootFiberNode.stateNode === fiberRoot
我们可以从HostRoot来访问和探索整个Fiber树。或者可以通过组件的实例中获得单个Fiber节点
compInstance._reactInternalFiber
Fiber节点结构
ClickCounter组件的Fiber节点结构:
{
stateNode: new ClickCounter,
type: ClickCounter,
alternate: null,
key: null,
updateQueue: null,
memoizedState: {count: 0},
pendingProps: {},
memoizedProps: {},
tag: 1,
effectTag: 0,
nextEffect: null
}
span DOM元素的Fiber节点结构:
{
stateNode: new HTMLSpanElement,
type: "span",
alternate: null,
key: "2",
updateQueue: null,
memoizedState: null,
pendingProps: {children: 0},
memoizedProps: {children: 0},
tag: 5,
effectTag: 0,
nextEffect: null
}
Fiber节点上有很多字段。我们之前已经描述了alternate(备份节点),effectTag(记录与之关联的effects), nextEffect(连接具有effects的Fiber节点使其成为线性节点)
stateNode属性
保留对class组件实例的引用, DOM节点或其他与Fiber节点相关联的React元素类实例的引用。一般来说, 我们可以说这个属性被用于保存与当前Fiber相关的本地状态。
type属性
定义与此Fiber节点相关联的函数或者类。对于class组件,type属性指向构造函数。对于DOM元素,type属性指向HTML标记。我经常用这个字段来判断这个Fiber节点与那个元素相关。
tag属性
定义Fiber节点的类型。在协调期间使用它确定需要做的"work"。如之前所述"work"取决于React元素的类型。createFiberFromTypeAndProps函数将React元素映射成相对应的Fiber节点类型。
在我们的例子中。ClickCounter的tag为1,表示为ClassComponent。span的tag为5,标记为HostComponent。
updateQueue属性
state更新和回调,DOM更新的队列。
memoizedState属性
用于创建输出Fiber的state。在处理更新的时候,它映射的是当前界面上呈现的state。
memoizedProps属性
在上一次渲染过程中用来创建输出的Fiber props。
pendingProps属性
已经更新后的Fiber props。需要用于子组件和DOM元素。
key属性
一组children中的唯一标示。帮助React确定那些发生了更改,新增或删除。更详细的解释在这里
总结
完整的Fiber结构, 可以在这里看到,在上面的说明省略了很多的字段比如child,sibling并return。这三个字段是构成链表树结构的关键。以及expirationTime、childExpirationTime和mode,这些字段是特定于Scheduler的。
通用算法
React分两个阶段执行work:render(渲染)和 commit(提交)
在render(渲染)阶段,React将更新应用于通过setState或React.render调度的组件, 并找出需要在UI中更新的内容。
如果是初始渲染,React将为render方法返回的每一个元素创建新的Fiber节点。在之后的更新中,将重新使用和更新现有的Fiber节点。
render阶段会构建一个带有side-effects(副作用)的Fiber节点树。effects描述了下一个commit(提交)阶段需要完成的“work”。在commit(提交)阶段,React会使用标记有effects的Fiber节点并将其应用于实例上。遍历Effects list执行DOM更新和其他对用户可见的更改。
请切记,render阶段的工作是可以异步执行的,React根据可用时间处理一个或者多个Fiber节点。当发生一些更重要的事情时,React会停止并保存已完成的工作。等重要的事情处理完成后,React从中断处继续完成工作。但是有时可能会放弃已经完成的工作,从顶层重新开始。此阶段执行的工作是对用户是不可见的,因此可以实现暂停。但是在commit(提交)阶段始终是同步的它会产生用户可见的变化, 例如DOM的修改. 这就是React需要一次性完成它们的原因。
调用生命周期函数使用React的“work”之一。在render阶段调用这些生命周期方法:
- UNSAFE_componentWillMount (deprecated)
- UNSAFE_componentWillReceiveProps (deprecated)
- getDerivedStateFromProps
- shouldComponentUpdate
- UNSAFE_componentWillUpdate (deprecated)
- render
由于render阶段不会产生DOM更新之类的副作用,因此React可以异步地对组件进行异步处理更新(甚至可能在多个线程中进行)。但是带有UNSAFE_前缀的生命周期函数常常会被误用,开发者会把副作用添加到这些生命周期函数中。这可能会导致异步渲染出现问题
在commit阶段调用这些生命周期方法,这些生命周期方法在commit阶段执行,所以它们可能包含副作用并涉及DOM更新。
- getSnapshotBeforeUpdate
- componentDidMount
- componentDidUpdate
- componentWillUnmount
渲染(render)阶段
协调算法使用renderRoot函数从最顶层的HostRoot节点开始,跳过已经处理过的节点,直到找到work未完成的节点为止。例如, 当在组件树深处调用setState方法, React 从顶部开始快速的跳过所有父级节点直接获得调用setState方法的组件。
WorkLoop
所有的Fiber节点在render阶段都会在WorkLoop中被处理。这是循环同步部分的实现:
function workLoop(isYieldy) {
if (!isYieldy) {
while (nextUnitOfWork !== null) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
} else {...}
}
在上面的代码中,nextUnitOfWork保持了对workInProgress tree
中一个有工作要处理的Fiber节点的引用。在React遍历Fiber树时,会使用nextUnitOfWork判断是否有未完成"work"的Fiber节点。当节点处理完成“work”后,nextUnitOfWork会指向下一个Fiber节点的引用或者为null。当nextUnitOfWork为null时,React会退出WorkLoop,并准备进入到commit阶段。
有四个主要的方法用于遍历树,并启动或完成工作:
为了演示如何使用它们。请查看下面遍历Fiber树的演示动画。演示动画中使用了这些函数的简化实现。我们可以通过演示看到,首先处理子节点的“work”,然后处理父节点的“work”。
使用直线连接代表同级,使用折线连接的代笔子级
逐步拆分下React遍历Fiber树的过程(首先处理子节点的“work”,然后处理父节点的“work”):
- beginWork a1
- beginWork b1,completeWork b1
- beginWork b2
- beginWork c1
- beginWork d1, completeWork d1
- beginWork d2, completeWork d2
- completeWork c1
- completeWork b2
- beginWork b3
- beginWork c2,completeWork c2
- completeWork b3
- completeWork a1
这个是视频的连接, 从概念上将"begin"看成进入组件,“complete”看成离开组件。
我们首先看下beginWork和performUnitOfWork这两个函数:
function performUnitOfWork(workInProgress) {
let next = beginWork(workInProgress);
if (next === null) {
next = completeUnitOfWork(workInProgress);
}
return next;
}
function beginWork(workInProgress) {
console.log('work performed for ' + workInProgress.name);
return workInProgress.child;
}
performUnitOfWork从workInProgress tree中接收一个Fiber节点。然后调用beginWork开始处理Fiber节点的work。为了演示,这里只是log了Fiber节点的name字段表示work已经完成。函数beginWork总是返回指向循环中下一个子节点或null。
如果有下一个子节点, 它将在workLoop函数中分配给nextUnitOfWork。如果没有子节点,React就知道了到达了分支的结尾。就会完成当前Fiber节点的work。React会执行它兄弟节点的工作,最后回溯到父节点。这是在completeUnitOfWork中完成的。
function completeUnitOfWork(workInProgress) {
while (true) {
let returnFiber = workInProgress.return;
let siblingFiber = workInProgress.sibling;
nextUnitOfWork = completeWork(workInProgress);
if (siblingFiber !== null) {
// 如果有同级,则返回它。以继续执行同级的工作
return siblingFiber;
} else if (returnFiber !== null) {
// 回溯到上一级
workInProgress = returnFiber;
continue;
} else {
// 已经到了root节点
return null;
}
}
}
function completeWork(workInProgress) {
console.log('work completed for ' + workInProgress.name);
return null;
}
当workInProgress节点没有子节点时,会进入此函数。在完成当前Fiber的工作后,会检查是否有兄弟节点。如果有,返回同级的兄弟节点的指针,分配给nextUnitOfWork。React将会从兄弟节点开始工作。只有处理完子节点所有分支之后, 才会回溯到父节点(所有子节点处理完成后,才会回溯到父节点)。
从实现可以看出,completeUnitOfWork主要用于迭代,主要工作都是beginWork和completeWork函数中进行的。
完整的示例
这里是完整的示例(beginWork,performUnitOfWork,completeUnitOfWork,completeWork的简易实现)
// 首先构建链表树
const a1 = {name: 'a1', child: null, sibling: null, return: null};
const b1 = {name: 'b1', child: null, sibling: null, return: null};
const b2 = {name: 'b2', child: null, sibling: null, return: null};
const b3 = {name: 'b3', child: null, sibling: null, return: null};
const c1 = {name: 'c1', child: null, sibling: null, return: null};
const c2 = {name: 'c2', child: null, sibling: null, return: null};
const d1 = {name: 'd1', child: null, sibling: null, return: null};
const d2 = {name: 'd2', child: null, sibling: null, return: null};
a1.child = b1;
b1.sibling = b2;
b2.sibling = b3;
b2.child = c1;
b3.child = c2;
c1.child = d1;
d1.sibling = d2;
b1.return = b2.return = b3.return = a1;
c1.return = b2;
d1.return = d2.return = c1;
c2.return = b3;
// 当前的指针是a1
let nextUnitOfWork = a1;
workLoop();
// 开始工作循环
function workLoop() {
while (nextUnitOfWork !== null) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
}
function performUnitOfWork(workInProgress) {
let next = beginWork(workInProgress);
if (next === null) {
next = completeUnitOfWork(workInProgress);
}
return next;
}
function beginWork(workInProgress) {
log('work performed for ' + workInProgress.name);
return workInProgress.child;
}
function completeUnitOfWork(workInProgress) {
while (true) {
let returnFiber = workInProgress.return;
let siblingFiber = workInProgress.sibling;
nextUnitOfWork = completeWork(workInProgress);
if (siblingFiber !== null) {
return siblingFiber;
} else if (returnFiber !== null) {
workInProgress = returnFiber;
continue;
} else {
return null;
}
}
}
function completeWork(workInProgress) {
log('work completed for ' + workInProgress.name);
return null;
}
function log(message) {
let node = document.createElement('div');
node.textContent = message;
document.body.appendChild(node);
}
提交(commit)阶段
提交阶段从completeRoot开始。这是React更新DOM,调用getSnapshotBeforeUpdate,componentDidMount,componentDidUpdate,componentWillUnmount等生命周期的地方。
React进入这一阶段时,有两颗树(workInProgress tree和current tree)以及effects list。current tree
表示了当前屏幕上呈现的状态。render阶段遍历current tree
时会生成另一颗树,在源码中被称为finishWork或workInProgress,表示未来需要在屏幕上呈现的状态。workInProgress tree
和current tree
结构类似。
调试时,如何获取current tree
以及workInProgress tree
?
// current tree
// 从容器对象上获取FiberRoot对象
const fiberRoot = query('#container')._reactRootContainer._internalRoot
// 获取current tree
const currentTree = fiberRoot.current
// 获取workInProgress tree
const workInProgressTree = fiberRoot.current.alternate
提交(commit)阶段,主要执行commitRoot函数,执行以下的操作:
- 在有Snapshot标记的Fiber节点上调用getSnapshotBeforeUpdate生命周期方法。
- 在有Deletion标记的Fiber节点上调用componentWillUnmount生命周期方法。
- 执行所有的 DOM 插入, 更新, 删除。
- 将
workInProgress tree
设置为current tree
。 - 在有Placement标记的Fiber节点上调用componentDidMount生命周期方法。
- 在有Update标记的Fiber节点上调用componentDidUpdate生命周期方法。
在调用getSnapshotBeforeUpdate方法后,React将commit,Fiber树中所有的副作用。分为两步:
第一步,执行所有的DOM插入,更新,删除和ref卸载。然后将workInProgress tree
设置为current tree
树。这是在第一步完成之后,第二步之前完成的。因此在componentWillUnmount生命周期方法在执行期间,状态依然是更新之前的。而componentDidMount/componentDidUpdate执行时的状态是更新之后的。第二步,执行其他生命周期方法和ref回调,这些方法作为单独的过程被执行。
commitRoot方法的预览:
function commitRoot(root, finishedWork) {
// 用来执行getSnapshotBeforeUpdate
commitBeforeMutationLifecycles()
// 用户更新DOM,以及执行componentWillUnmount
commitAllHostEffects();
root.current = finishedWork;
// 调用componentDidUpdate和componentDidMount生命周期的地方
commitAllLifeCycles();
}
这些子函数,内部都包含了一个循环。循环遍历effects list,并检查effects的类型。当发现类型和子函数的目的相同时,就应用它。
Pre-mutation lifecycle methods
遍历effects list,并检查节点是否具有Snapshot effect的源代码:
function commitBeforeMutationLifecycles() {
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
if (effectTag & Snapshot) {
const current = nextEffect.alternate;
commitBeforeMutationLifeCycles(current, nextEffect);
}
nextEffect = nextEffect.nextEffect;
}
}
如果是class组件,调用getSnapshotBeforeUpdate生命周期方法。
DOM updates
commitAllHostEffects是React执行DOM更新的地方。React会把componentWillUnmount作为commitDeletion删除过程中的一部分。
function commitAllHostEffects() {
switch (primaryEffectTag) {
case Placement: {
commitPlacement(nextEffect);
...
}
case PlacementAndUpdate: {
commitPlacement(nextEffect);
commitWork(current, nextEffect);
...
}
case Update: {
commitWork(current, nextEffect);
...
}
case Deletion: {
commitDeletion(nextEffect);
...
}
}
}
Post-mutation lifecycle methods
commitAllLifecycles是React调用所有剩余生命周期方法componentDidUpdate和componentDidMount的地方。
总结
React源码很复杂,Max Koretskyi的这篇文章内容也很多,所有总结下这篇博客的要点:
- React的核心是跟踪组件状态变化并将更新后的状态更新到屏幕上。在React中,我们把这个过程称为 reconciliation (协调)。
- 协调期间的各种活动在Fiber架构中统称为work(工作),work的类型取决于React元素的类型,React元素的类型取决于React.createElement函数的第一个参数类型。
- 协调期间React元素会被合并到Fiber节点树之中,每一种React元素都会一种对应的Fiber节点。Fiber节点不会重复创建,会在第一次渲染后重用。
- React会为每一个React元素创建一个Fiber节点,我们会得到一个Fiber节点树。Fiber节点树是通过链表的形式存储的。每一个Fiber节点都拥有child,sibling和return字段,用于中断恢复遍历。
- 在初次渲染时会生成一棵Fiber树。被称为
current tree
。当开始更新时,React会构建一个workInProgress tree
。 current tree
代表了当前的状态,workInProgress tree
代表了未来的状态。- 在commit阶段
workInProgress tree
会被设置为current tree
。 - React在遍历
current tree
时,会为每一个Fiber节点创建一个alternate字段,alternate字段保存了Fiber节点的备份,alternate字段上保存的备份Fiber节点构成了workInProgress tree
。 - 数据获取、订阅或者手动修改过DOM都统称为副作用(side-effects)或者简称为“作用”(effects)。因为它们会影响其他组件,并且在渲染期间无法完成。
- effects是一种work类型。因此Fiber节点是一种跟踪更新和effects的便捷机制,每一个Fiber节点都有与之相关联的effects。它们被编码在effectTag字段之中。Fiber中的effects定义了处理更新之后需要做的"work"。对于DOM元素,"work"包含了添加,更新,删除。对于类组件,包括了更新ref,调用componentDidMount和componentDidUpdate生命周期方法。还有其他effects对应于其他类型的Fiber。
- React会将具有effects的Fiber节点,构建为线性列表,以方便快速迭代。firstEffect是列表的开始位置,使用nextEffect属性将节点链接在一起(构建成线性列表)。
- 可以通过容器DOM元素,获取FiberRoot对象
query('#container')._reactRootContainer._internalRoot
。 - FiberRoot对象的current属性,是current tree的第一个节点,被称为hostRoot。
fiberRoot.current
。 - hostRoot节点的stateNode属性,指向FiberRoot对象。
- 获取
workInProgress tree
,fiberRoot.current.alternate
。 - Fiber节点结构见上文。
- React分两个阶段执行work:render(渲染)和 commit(提交)
- render阶段的工作是可以异步执行的,React根据可用时间处理一个或者多个Fiber节点。当发生一些更重要的事情时,React会停止并保存已完成的工作。等重要的事情处理完成后,React从中断处继续完成工作。但是有时可能会放弃已经完成的工作,从顶层重新开始。此阶段执行的工作是对用户是不可见的,因此可以实现暂停。
- commit(提交)阶段始终是同步的它会产生用户可见的变化, 例如DOM的修改. 这就是 React需要一次性完成它们的原因。
- 所有的Fiber节点在render阶段都会在WorkLoop中被处理。WorkLoop中主要有四个函数,performUnitOfWork,beginWork,completeUnitOfWork,completeWork。WorkLoop首先处理子节点的“work”,所有子节点处理完成后,回溯然后处理父节点的“work”。从概念上将"begin"看成进入组件,“complete”看成离开组件。
- 提交(commit)阶段分为两步实现。第一步,执行所有的DOM插入,更新,删除和ref卸载以及执行componentWillUnmount生命周期方法。第二步,执行其他生命周期方法和ref回调,这些方法作为单独的过程被执行。第一步和第二步中间会将
workInProgress tree
设置为current tree
树。