前言
在本文中我们会看到React如何处理state的更新。以及如何构建effects list。我们会详细介绍render(渲染)阶段以及commit(提交)阶段发生的事情。
我们会在completeWork函数中看到React如何:
- 更新state属性。
- 调用render方法并比较子节点。
- 更新React元素的props。
并且在commitRoot函数中React如何:
- 更新元素的textContent属性。
- 调用componentDidUpdate生命周期方法。
但在这之前,让我们先来看看调用setState时React是如何安排工作。我们使用之前文章的一个例子,方便文章讲解,一个简单的计数器组件:
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};
});
}
componentDidUpdate() {}
render() {
return [
<button key="1" onClick={this.handleClick}>Update counter</button>,
<span key="2">{this.state.count}</span>
]
}
}
调度更新
当我们点击按钮的时候,click事件被触发,React会执行我们的回调。在我们的应用程序中,他会更新我们的state。
class ClickCounter extends React.Component {
...
handleClick() {
this.setState((state) => {
return {count: state.count + 1};
});
}
}
每一个React组件,都有一个关联的updater
(更新器)。updater
充当了组件和React core
之间的桥梁。这允许setState在ReactDOM,React Native,服务器端渲染和测试用例中有不同的方式实现。
在这里,我们将会关注ReactDOM中updater
(更新器)的实现。它使用了Fiber reconciler
。对于ClickCounter组件,它是classComponentUpdater。它负责检索Fiber实例,队列化更新,调度工作。
在更新时,更新器会在Fiber节点上添加更新队列。在我们的例子中,ClickCounter组件对应的Fiber节点的结构如下:
{
stateNode: new ClickCounter, // 保留对class组件实例的引用
type: ClickCounter, // type属性指向构造函数
updateQueue: { // state更新和回调,DOM更新的队列。
baseState: {count: 0}
firstUpdate: {
next: {
payload: (state) => { return {count: state.count + 1} }
}
},
...
},
...
}
如你所见updateQueue.firstUpdate.next.payload的内容是我们在setState中传递的回调。它表示在render阶段中需要处理的第一个更新。
处理ClickCounter的Fiber节点的更新
在之前的文章中介绍了nextUnitOfWork全局变量的作用。
nextUnitOfWork保持了对workInProgress tree中一个有工作要处理的Fiber节点的引用。nextUnitOfWork会指向下一个Fiber节点的引用或者为null。可以使用nextUnitOfWork变量判断是否有没有完成工作的Fiber节点。
我们假设以及调用了setState,React将setState的回调添加到了ClickCounter的Fiber节点的updateQueue字段中并调度了工作。React进入了render(渲染)阶段。它从HostRoot节点开始,使用renderRoot函数遍历Fiber树。它会跳过已经处理过Fiber节点,直到找到工作未完成的节点。此时,只有一个Fiber节点有未完成工作,就是ClickCounter Fiber节点。
Fiber树的第一个节点是一种特殊的类型节点,叫做HostRoot。它在内部创建,是最顶层组件的父组件
所有的“work”都会在Fiber节点的备份上进行。备份存储在alternate字段中。如果尚未创建备份节点,React会在处理更新之前,使用createWorkInProgress函数创建备份。假设nextUnitOfWork拥有ClickCounter的Fiber节点的引用。
beginWork
首先Fiber进入beginWork
函数。
由于beginWork函数是每一个Fiber节点都会执行的。因此如果需要调试render阶段的源码,这里是放置断点的好地方。我(指Max)经常那么做。
beginWork函数内部是一个巨大的switch语句,switch语句通过Fiber节点的tag属性,判断Fiber节点的类型。然后执行相应的函数执行工作。
我们的节点是CountClicks组件的Fiber节点,所以会进入ClassComponent的分支语句
function beginWork(current$$1, workInProgress, ...) {
...
switch (workInProgress.tag) {
...
case FunctionalComponent: {...}
case ClassComponent:
{
...
return updateClassComponent(current$$1, workInProgress, ...);
}
case HostComponent: {...}
case ...
}
然后我们进入updateClassComponent函数
在updateClassComponent函数中,判断组件要么是首次渲染,还是恢复工作(render阶段可以被打断)还是更新。React要么创建实例并挂载这个组件,要么仅仅更新它。
function updateClassComponent(current, workInProgress, Component, ...) {
...
const instance = workInProgress.stateNode;
let shouldUpdate;
if (instance === null) {
...
// 如果实例为null, 我们需要构造实例
constructClassInstance(workInProgress, Component, ...);
mountClassInstance(workInProgress, Component, ...);
shouldUpdate = true;
} else if (current === null) {
// 在重新开始后,我们已经有了一个可以重用的实例。
shouldUpdate = resumeMountClassInstance(workInProgress, Component, ...);
} else {
// 只是进行更新
shouldUpdate = updateClassInstance(current, workInProgress, ...);
}
return finishClassComponent(current, workInProgress, Component, shouldUpdate, ...);
}
处理CountClicks Fiber的更新
经过beginWork
和updateClassComponent
函数,我们已经有了ClickCounter组件的实例,我们此时进入updateClassInstance。updateClassInstance函数是React处理类组件大部分工作的地方。以下是函数中执行的最重要的操作(按执行顺序排列):
- 执行UNSAFE_componentWillReceiveProps生命周期函数
- 执行Fiber节点中的updateQueue的更新队列,生成新的的state
- 使用新的state,执行getDerivedStateFromProps并获取结果
- 执行shouldComponentUpdate判断组件是否需要更新。如果是false,跳过整个render处理,包括此组件以及子组件。如果是true,继续更新。
- 执行UNSAFE_componentWillUpdate生命周期函数
- 添加effect用来触发componentDidUpdate生命周期函数
- 在组件实例上更新state和props
尽管componentDidUpdate的effect在render阶段添加,但是该方法将在下一个commit阶段被执行state和props应该在组件实例的render方法调用之前被更新,因为render方法的输出通常依赖于state和props,如果我们不这样做,它将每次都返回相同的输出。
// 简化后的代码
function updateClassInstance(current, workInProgress, ctor, newProps, ...) {
// 组件的实例
const instance = workInProgress.stateNode;
// 之前的props
const oldProps = workInProgress.memoizedProps;
instance.props = oldProps;
if (oldProps !== newProps) {
// 如果当前的props和之前的props有差异,执行UNSAFE_componentWillReceiveProps
callComponentWillReceiveProps(workInProgress, instance, newProps, ...);
}
// 更新队列
let updateQueue = workInProgress.updateQueue;
if (updateQueue !== null) {
// 执行更新队列,获取新的状态
processUpdateQueue(workInProgress, updateQueue, ...);
// 获取最新的state
newState = workInProgress.memoizedState;
}
// 使用最新的state,调用getDerivedStateFromProps
applyDerivedStateFromProps(workInProgress, ...);
// 获取最新的state(getDerivedStateFromProps可能会更新state)
newState = workInProgress.memoizedState;
// 执行shouldComponentUpdate,判断组件是否需要更新
const shouldUpdate = checkShouldComponentUpdate(workInProgress, ctor, ...);
if (shouldUpdate) {
// 如果需要更新执行UNSAFE_componentWillUpdate生命吗周期函数
instance.componentWillUpdate(newProps, newState, nextContext);
// 并且添加effect,在commit阶段会执行componentDidUpdate,getSnapshotBeforeUpdate
workInProgress.effectTag |= Update;
workInProgress.effectTag |= Snapshot;
}
// 更新props和state
instance.props = newProps;
instance.state = newState;
return shouldUpdate;
}
上面的代码片段是简化后的代码,例如在调用生命周期函数之前或添加effect。React使用typeof操作符检测组件是否实现了这个方法。例如React检测是否实现了componentDidUpdate,在effect添加之前
if (typeof instance.componentDidUpdate === 'function') {
workInProgress.effectTag |= Update;
}
好的,现在我们知道了ClickCounter的Fiber节点在render(渲染)阶段,执行了那些操作。现在让我们看看Fiber节点上的值是如何被改变的。在React开始工作时,ClickCounter组件的Fiber节点如下:
{
effectTag: 0,
elementType: class ClickCounter,
firstEffect: null,
memoizedState: {count: 0},
type: class ClickCounter,
stateNode: {
state: {count: 0}
},
updateQueue: {
baseState: {count: 0},
firstUpdate: {
next: {
payload: (state, props) => {…}
}
},
...
}
}
工作完成之后,我们得到如下的Fiber节点
{
effectTag: 4,
elementType: class ClickCounter,
firstEffect: null,
memoizedState: {count: 1},
type: class ClickCounter,
stateNode: {
state: {count: 1}
},
updateQueue: {
baseState: {count: 1},
firstUpdate: null,
...
}
}
观察一下两者的对比。memoizedState的属性值由0变为了1。updateQueue的baseState的属性值由0变为了1。updateQueue没有队列更新,firstUpdate为null。并且我们修改effectTag的值,标记了我们在commit阶段执行的副作用。
effectTag由0变为了4,4在二进制中是0b00000000100
, 这代表第三个位被设置,而这一位代表Update
export const Update = 0b00000000100;
总结,ClickCounter组件的Fiber节点,在render阶段做了调用前置突变生命周期方法,更新state以及定义相关副作用。
ClickCounter Fiber的子协调
完成上面的工作后,React进入finishClassComponent函数, 这是React调用组件的render方法,并对子级应用diff算法的地方。React文档对diff算法有大致的概述。
如果深入了解,我们可以知道React中的diff算法实际将React元素与Fiber节点进行了比较。过程非常的复杂(原文作者没有在这里进行过多的叙述),在我们的例子中,render方法返回React元素数组,所以如果你想了解更多的细节可以查看React源码的reconcileChildrenArray函数。
此时有两个重要的事情需要了解。
- React在进行子协调的过程时,创建或者更新了子元素的Fiber节点。子元素由render返回。finishClassComponent方法返回的当前节点的第一个子节点的引用。引用会被分配给nextUnitOfWork变量,然后在workLoop中进行处理。
- React更新了子级的props,这是父级工作的一部分。为此,它使用从render方法返回的React元素中的数据。
例如,React进行子协调前,span对应的Fiber节点
{
stateNode: new HTMLSpanElement,
type: "span",
key: "2",
memoizedProps: {children: 0}, // 上一次用于渲染的props
pendingProps: {children: 0}, // 更新后的props,需要用于dom和子组件上
...
}
这是调用render方法后返回React元素结构,Fiber节点和返回的React元素的props有点不同,在创建Fiber节点备份时,createWorkInProgress函数会将React元素更新的数据同步到Fiber节点上。(Fiber上的工作都是在备份节点上进行的)
{
$$typeof: Symbol(react.element)
key: "2"
props: {children: 1}
ref: null
type: "span"
}
ClickCounter组件在完成子协调后,span元素的Fiber节点将会更新,结构如下:
{
stateNode: new HTMLSpanElement,
type: "span",
key: "2",
memoizedProps: {children: 0}, // 在上一次渲染过程中用来创建输出的Fiber props。
pendingProps: {children: 1}, // 已经更新后的Fiber props。需要用于子组件和DOM元素。
...
}
稍后在执行span元素的Fiber节点的工作时,会将pendingProps拷贝到memoizedProps上,并添加effects,方便commit阶段更新dom。
好了,这就是ClickCounter组件在render阶段执行的所有工作。由于button元素是ClickCounter组件的第一个子元素,所有button元素的Fiber节点被分配给了nextUnitOfWork。由于button元素的Fiber节点没有工作,所以React会将它的兄弟节点即span元素的Fiber节点分配给nextUnitOfWork。此处的行为在completeUnitOfWork方法中。
处理span Fiber的更新
nextUnitOfWork目前指向span Fiber备用节点(因为工作都是在workInProgress tree
上完成的)。处理的步骤和ClickCounter类似,我们从beginWork函数开始。
有span元素的Fiber节点的tag属性是HostComponent类型,beginWork进入了updateHostComponent分支。(这部分的内容,以及与当前版本的React有了冲突)
function beginWork(current$$1, workInProgress, ...) {
...
switch (workInProgress.tag) {
case FunctionalComponent: {...}
case ClassComponent: {...}
case HostComponent:
return updateHostComponent(current, workInProgress, ...);
case ...
}
span Fiber的子协调
在我们的例子中,span的Fiber在子协调中没有什么重要的事情发生
span Fiber完成工作
beginWork完成后,span Fiber进入completeWork。但是在此之前React需要更新span Fiber上的memoizedProps。在子协调时,React更新了span Fiber的pendingProps字段。(这部分的内容已经与现在React版本有所冲突)
{
stateNode: new HTMLSpanElement,
type: "span",
key: "2",
memoizedProps: {children: 0},
pendingProps: {children: 1},
...
}
span Fiber的beginWork完成后,就会将pendingProps更新到memoizedProps上
function performUnitOfWork(workInProgress) {
...
next = beginWork(current$$1, workInProgress, nextRenderExpirationTime);
workInProgress.memoizedProps = workInProgress.pendingProps;
...
}
然后调用completeWork方法,completeWork方法内部也是一个大的switch语句
function completeWork(current, workInProgress, ...) {
...
switch (workInProgress.tag) {
case FunctionComponent: {...}
case ClassComponent: {...}
case HostComponent: {
...
updateHostComponent(current, workInProgress, ...);
}
case ...
}
}
由于span Fiber是HostComponent,所以会执行updateHostComponent函数,在这个函数中React会执行以下的操作:
- 准备DOM更新
- 它们添加到span Fiber的updateQueue
- 添加DOM更新的effects
在执行这些操作前,Fiber节点结构:
{
stateNode: new HTMLSpanElement,
type: "span",
effectTag: 0
updateQueue: null
...
}
操作后Fiber节点的结构:
{
stateNode: new HTMLSpanElement,
type: "span",
effectTag: 4,
updateQueue: ["children", "1"],
...
}
注意Fiber节点的effectTag值,由0变为了4,4在二进制中是100,这是Update副作用的表示位,这是commit阶段React需要做的工作,而updateQueue字段的负载(payload)将会在更新时用到。
React在依次完成子元素工作和ClickCounter工作后,就完成了render阶段。此时React将 workInProgress tree(备份节点, render阶段更新的树)分配给FiberRoot节点的finishedWork属性。这是一颗新的需要刷新在屏幕上的树,它可以在render阶段之后立即处理,或者挂起等待浏览器的空闲时间。
effects list
在我们的例子中span节点和ClickCounter节点,具有副作用。HostRoot(Fiber树的一个节点)上的firstEffect属性指向span Fiber节点。
React在compliteUnitOfWork函数中创建effects list这是一个带有effects的Fiber树。effects中包含了: 更新span的文本,调用ClickCounter生命周期函数。
effects list线性列表:
commit阶段
commit阶段从completeRoot函数开始,在开始任何工作前,它将FiberRoot的finishedWork属性设置为null。
commit阶段始终是同步的,所以它可以安全的更新HostRoot来指示commit开始了。
commit阶段,是React更新DOM, 以及调用生命周期方法的地方。为此React将遍历上一个render阶段构造的effects list,并应用它们。
在render阶段,span和ClickCounter的effects如下:
{ type: ClickCounter, effectTag: 5 }
{ type: 'span', effectTag: 4 }
ClickCounter Fiber节点的effectTag值是5,5的二进制是101,第三位被设置为1,这是Update副作用的表示位,我们需要调用componentDidUpdate生命周期方法。最低位也是1,表面Fiber节点在render阶段的所有工作都已经完成。
span Fiber节点的effectTag值是5,5的二进制是101,第三位被设置为1,这是Update副作用的表示位,我们需要更新span元素的textContent。
应用effects
应用effects在函数commitRoot中完成,commitRoot由三个子方法组成。
function commitRoot(root, finishedWork) {
commitBeforeMutationLifecycles()
commitAllHostEffects();
root.current = finishedWork;
commitAllLifeCycles();
}
每一个子方法都会循环effects list, 并检查effects的类型。
第一个函数commitBeforeMutationLifeCycles检查Snapshot effects,并且调用getSnapshotBeforeUpdate函数,但是我们在ClickCounter组件中没有添加getSnapshotBeforeUpdate生命周期函数,所以React不会在render阶段添加这个作用,所以在我们的例子中,这个方法什么都没有做。
DOM更新
接下来是commitAllHostEffects, 在这个函数中span的文本内容会从0到1。ClickCounter Fiber节点没有动作。
commitAllHostEffects内部也是一个大的switch,根据effects的类型,应用相应的操作:
function updateHostEffects() {
switch (primaryEffectTag) {
case Placement: {...}
case PlacementAndUpdate: {...}
case Update:
{
var current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Deletion: {...}
}
}
进入到commitWork函数,最近进入updateDOMProperties方法,它会使用在render阶段添加在Fiber节点上的updateQueue属性中的负载(payload),并将其应用在span元素的textContent属性上。
function updateDOMProperties(domElement, updatePayload, ...) {
for (let i = 0; i < updatePayload.length; i += 2) {
const propKey = updatePayload[i];
const propValue = updatePayload[i + 1];
if (propKey === STYLE) { ...}
else if (propKey === DANGEROUSLY_SET_INNER_HTML) {...}
else if (propKey === CHILDREN) {
setTextContent(domElement, propValue);
} else {...}
}
应用DOM更新后,React将finishedWork上的workInProgress tree
分配给HostRoot。将workInProgress tree
设置为current tree
。
root.current = finishedWork;
调用后置生命周期函数
最后剩下的函数是commitAllLifecycles。在render阶段React会将Update effects添加到ClickCounter组件中,commitAllLifecycles函数是调用后置突变生命周期方法的地方。
function commitAllLifeCycles(finishedRoot, ...) {
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
if (effectTag & (Update | Callback)) {
const current = nextEffect.alternate;
commitLifeCycles(finishedRoot, current, nextEffect, ...);
}
if (effectTag & Ref) {
commitAttachRef(nextEffect);
}
nextEffect = nextEffect.nextEffect;
}
}
生命周期函数在commitLifeCycles函数中调用。
function commitLifeCycles(finishedRoot, current, ...) {
...
switch (finishedWork.tag) {
case FunctionComponent: {...}
case ClassComponent: {
const instance = finishedWork.stateNode;
if (finishedWork.effectTag & Update) {
if (current === null) {
instance.componentDidMount();
} else {
...
instance.componentDidUpdate(prevProps, prevState, ...);
}
}
}
case HostComponent: {...}
case ...
}
你可以在这个方法中看到,React为第一次渲染的组件调用componentDidMount方法。
总结
Max Koretskyi的文章内容较多,所以最后总结下知识点:
- 每一个组件都有一个与之关联的
updater
(更新器)。更新器充当了组件和React core
之间的桥梁。这允许setState在ReactDOM,React Native,服务器端渲染和测试用例中有不同的方式实现。 - 对于class组件,
updater
(更新器)是classComponentUpdater - 在更新时,
updater
(更新器)会在Fiber节点的updateQueue
属性中,添加更新队列。 render
(渲染)阶段,React会从HostRoot
开始遍历Fiber树, 跳过已经处理过的Fiber节点,直到找到还有work
没有完成的Fiber节点。所有工作都是在Fiber节点的备份上进行, 备份存储在Fiber节点的alternate
字段上。如果alternate
字段还没有创建, React会在处理工作前使用createWorkInProgress
创建alternate
字段,createWorkInProgress
函数会将React元素的状态同步到Fiber节点上。nextUnitOfWork
保持了对workInProgress tree
中一个有工作要处理的Fiber节点的引用。- Fiber节点进入
beginWork
函数,beginWork
函数会根据Fiber节点类型执行相对应的工作,class组件会被updateClassComponent
函数执行 。 - 每一个Fiber节点都会执行
beginWork
函数。经过beginWork
函数,组件要么被创建组件实例子,要么只是更新组件实例。 经过
beginWork
,updateClassComponent
后,进入updateClassInstance
,这里是处理类组件大部分work
的地方。(下面的操作按顺序执行)- 执行UNSAFE_componentWillReceiveProps生命周期函数
- 执行Fiber节点中的updateQueue的更新队列,生成新的的state
- 使用新的state,执行getDerivedStateFromProps并获取返回结果
- 执行shouldComponentUpdate判断组件是否需要更新。如果是false,跳过整个render处理,包括此组件以及子组件。如果是true,继续更新。
- 执行UNSAFE_componentWillUpdate生命周期函数
- 添加effects用来触发componentDidUpdate生命周期函数(
commit
阶段才会触发) - 在组件实例上更新state和props
- render阶段class组件主要做了:调用前置突变生命周期方法,更新state,定义相关effects。
- 完成
updateClassInstance
结束后,React进入finishClassComponent
, 这里React调用组件的render方法,并对子级应用diff算法的地方(子协调的地方)。 - 子协调会创建或者更新子元素的Fiber节点,子元素由render方法返回,子元素的属性会被同步到子元素的Fiber节点上。
finishClassComponent
会返回第一个子元素的Fiber节点,并分配给nextUnitOfWork
, 方便之后在workLoop
中继续处理(之后会子节点的节点)。 - 更新子元素props是在父元素工作中的一部分。
- render阶段完成后。React将
workInProgress tree
(备份节点, render阶段更新的树)分配给FiberRoot
节点的finishedWork
属性。 commi
t阶段之前,FiberRoot
的finishedWork
属性设置为null
。commit
阶段是同步的,是React更新DOM, 以及调用生命周期方法的地方(应用副作用)。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。