function beginWork(current$$1, workInProgress, ...) {
...
switch (workInProgress.tag) {
case FunctionalComponent: {...}
case ClassComponent: {...}
case HostComponent:
return updateHostComponent(current, workInProgress, ...);
case ...
}
function updateHostEffects() {
switch (primaryEffectTag) {
case Placement: {...}
case PlacementAndUpdate: {...}
case Update:
{
var current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Deletion: {...}
}
}
在我之前的文章【译】深入研究 React 的 Fiber 协调算法中,我已经介绍了更新过程的一些技术细节,为理解本篇文章打下了基础。
我概述了一些主要的数据结构和概念,特别是
Fiber nodes
,current
、work-in-progress tree
,side-effects
和effects list
。我也总结了主要的算法,并分别解释了render
和commit
两个阶段的工作。如果你还没阅读过,我建议你先阅读先前的文章。我也提供了一个用于示例的应用,它有一个按钮,可以点击增加计数并渲染到屏幕:
你可以在这里在线运行。它在 React 中实现为一个简单的组件,会在
render
方法返回button
和span
两个子元素。当你点击按钮的时候,事件处理器内部会触发组件状态的更新,最终导致span
元素内部的文本更新:这里添加了
componentDidUpdate
的生命周期方法。我们需要理解,React 在commit
阶段是如何添加 effects 并调用这个生命周期的。在这篇文章,我想要向你们说明 React 是如何处理状态更新和构建
effects list
的。我们将了解在render
和commit
阶段中一些主要函数中做的事情。特别地,我们将了解 completeWork 函数中会处理以下这些工作:
ClickCounter
的count
状态render
方法去返回子项和执行比较span
元素的 props而 commitRoot 中则会:
span
元素的textContent
属性componentDidUpdate
生命周期方法但在这之前,我们先了解一下,在点击事件中调用
setState
的时候,React 是如何调度这些工作的。Scheduling updates
当我们点击按钮的时候,
click
事件触发,之后 React 会执行我们通过 props 传入 button 的回调函数。在我们的示例中,它只是增加了计数然后更新状态:每个 React 组件都有其相对应的
updater
,它充当了组件和 React core 之间的一个桥梁。有了这个,才能允许setState
方法在不同环境分别实现对应的逻辑,比如 ReactDOM,React Native,server side rendering(服务端渲染),testing utilities(测试工具)。在这篇文章中,我们会探究 ReactDOM 中
updater
这个对象的实现,它是基于 Fiber reconciler 的。具体到ClickCounter
组件,它实际是 classComponentUpdater。它的主要作用是创建 Fiber 的实例、将更新加入队列和调度工作。当一个更新被加入队列的时候,它实际是将其加入到对应的 Fiber 节点的更新队列上。在我们的示例中,
ClickCounter
组件对应的 Fiber 节点 会有如下结构:正如你所看到的,
updateQueue.firstUpdate.next.payload
是ClickCounter
组件中传给setState
的函数。它表示在render
节点需要处理的第一个更新。Processing updates for the ClickCounter Fiber node
在之前的文章的wook loop 章节 我解释了全局变量
nextUnitOfWork
的作用。它其实指向了workInProgress tree
中还有工作需要处理的 Fiber 节点。尤其,当遍历 Fiber 树的时候,根据这个变量,React 就可以知道哪些 Fiber 节点还有未完成的工作。让我们假设
setState
方法已经被调用。React 会把setState
中的回调加入ClickCounter
对应的 fiber 节点中的updateQueue
,然后调度工作。之后,React 进入render
阶段,它从顶层的HostRoot
fiber 节点出发,使用 renderRoot 开始遍历。它会跳过(bails out)所有已经处理的节点,直到找到未完成工作的节点。在示例中,这里只有ClickCounter
这个 fiber 节点需要处理。所有的工作都是在 Fiber 副本节点上执行的,fiber 节点上的
alternate
属性指向这个副本。如果在处理更新之前这个alternate
节点还未创建,React 会先使用 createWorkInProgress 函数来创建一个副本。这里,我们先假设nextUnitOfWork
保留了指向ClickCounter
fiber 副本节点的一个引用。beginWork
首先,Fiber 会从 beginWork 函数开始。
beginWork
函数内部其实是一个大的switch
语句,它通过tag
找到 fiber 节点对应需要执行的工作类型,然后执行相对应的函数。在CountClick
是一个类组件,因此,它匹配的分支如下:在这之后,我们进入 updateClassComponent 函数。根据这个节点是否是首次渲染,React 会决定是要继续更新工作,或者创建一个实例并挂载。
Processing updates for the ClickCounter Fiber
我们已经有了
ClickCounter
组件的实例,接下来,我们进入 updateClassInstance 方法。React 对类组件的大部分工作都是在这里执行的。按照函数内部的执行的顺序,几个最重要的操作如下:UNSAFE_componentWillReceiveProps()
(deprecated)updateQueue
中的更新,生成新的状态getDerivedStateFromProps
并得到执行的结果shouldComponentUpdate
确保组件想要更新;如果返回false
跳过整个渲染流程,包括调用整个组件及其子项的render
方法UNSAFE_componentWillUpdate
(deprecated)componentDidUpdate
添加到 effect 列表state
和props
组件实例的
state
和props
必须在调用render
方法之前更新,因为render
的结果是基于state
和props
的。如果不这样做,每次都会返回相同的输出结果。这个函数的简化版本如下:
在上面的代码片段中我删除了一些辅助的代码。比如,在调用生命周期或者添加 effect 之前,React 会先使用
typeof
检查组件是否实现了对应的方法。例如下面的例子展示了 React 在添加 effect 前是如何检查是否有componentDidUpdate
方法的:到这里,我们已经知道在 render 阶段,
ClickCounter
Fiber 节点上进行了哪些操作。接下来,我们看下这些操作影响到了节点上的哪些属性的值。在 React 开始处理更新工作之前,这个 fiber 节点如下:在这个阶段结束后,我们得到 fiber 节点:
比较一下这些属性值有什么差别。
在更新被应用之后,
memoizedState
和updateQueue
中的baseState
上的count
属性被更新为1
。React 同时也更新了ClickCounter
组件实例上的状态。执行结束之后,更新队列上没有需要执行的更新,所以
firstUpdate
为null
。还有一个重要的变化是,我们改变了effectTag
的值。它从0
变为4
。使用二进制表示是100
,意味着第三位被置为 1,对应了 side-effect tag 中的Update
操作:我们总结一下,当处理父节点
ClickCounter
的时候,React 调用了需要在更新前执行的生命周期方法,更新状态并定义相关的 side-effects。Reconciling children for the ClickCounter Fiber
当父节点的工作执行完毕,React 会进入 finishClassComponent 方法。这里,React 会调用实例上的
render
方法对组件返回的 children 应用 diffing 算法。在官方文档中描述了这个算法的核心思想。相关的部分如下:如果我们深入理解的话,这里实际比较的是 Fiber 节点上的 React 元素。这个过程是相当复杂的,我们先不查看一些具体的实现细节。我会把这个过程分成几个小的部分来讲解,特别关注 child 的 reconciliation 过程。
首先,有两个重要的工作我们需要理解:
render
方法返回的子元素创建一个新的 Fiber 节点,或者更新已有的 Fiber 节点。finishClassComponent
函数会返回当前 Fiber 节点上的第一个孩子节点的引用。它会被赋值给nextUnitOfWork
,并在之后的 work loop 中被处理。render
方法返回的 React 元素。当
ClickCounter
组件的 children 已经完成了协调,span
上的pendingProps
会更新,这和span
元素上的值是相对应的:在这之后,当 React 处理
span
Fiber 节点上的工作时,pendingProps
的值会被拷贝到memoizedProps
上,然后添加更新 DOM 的 effect。这就是在 render 阶段,React 对
ClickCounter
执行的全部工作。因为ClickCouner
的第一个孩子是 button,所以它会被赋值给nextUnitOfWork
变量。如果不需进行任何工作,React 会处理它的兄弟节点,也就是span
节点。根据这里描述的算法,这个过程是在componentUnitOfWork
函数中处理的。Processing updates for the Span fiber
现在
nextUnitOfWork
指向了span
fiber 节点的副本(alternate),React 会从这里开始工作。和之前处理ClickCounter
类似,我们会从 beginWork 这个函数开始。因为
span
节点是HostComponent
类型,它会进入 switch 语句的这个分支:最后执行 updateHostComponent 函数。你可以比较下这个函数和处理类组件的
updateClassComponent
函数的区别。另外地,对于函数组件,则会调用updateFunctionComponet
方法,其他的以此类推。所有相关的函数可以在 ReactFiberBeginWork.js 这个文件中找到。Reconciling children for the span fiber
在我们的例子中,
span
节点上的updateHostComponent
函数并不需要处理什么工作。Completing work for the Span Fiber node
当
beginWork
执行完毕,这些节点会进入completeWork
函数。但在这之前,React 需要更新 span 节点上的memoizedProps
。你或许记得,之前在处理ClickCounter
组件的子节点协调过程中,React 已经更新了span
Fiber 节点上的pendingProps
:因此,当
span
fiber 的beginWork
执行完毕,React 会更新memoizedProps
:之后,会调用
completeWork
函数,这和前面的beginWork
类似,这个函数内部也是一个大的switch
语句:因为
span
Fiber 节点是HostComponent
,它会执行 updateHostComponent 函数。函数内部执行了以下工作:span
fiber 节点上的更新添加到updateQueue
在操作执行前,
span
的 Fiber 节点是这样的:当工作执行完毕,它是这样的:
这里的主要区别是
effectTag
和updateQueue
的值。它从0
变为4
。使用二进制表示是100
,意味着第三位被置为 1,对应了 side-effect tag 中的Update
操作。这是 React 在 commit 阶段唯一需要做的任务。updateQueue
保存了更新所需的数据。当 React 处理完
ClickCounter
和它的子节点之后,render
阶段就执行完毕了。我们可以把这个已完成的副本(alternate)树赋值给FiberRoot
上的finishedWork
属性。这棵新的树需要被更新到屏幕上。这个工作会在render
阶段结束后立即执行,如果当前 React 需要为浏览器让出时间,这个工作则会在之后执行。Effects list
在我们的例子中,因为
span
节点 和ClickCounter
组件都有副作用需要执行,React 会将HostFiber
上的firstEffect
属性指向span
的 Fiber 节点。React 会在 completeUnitOfWork 这个函数中构建
effect list
。以下是一棵需要执行更新副作用的 Fiber 树,它会更新span
的文本内容,并调用ClickCounter
的生命周期:这些 effect 表示为线性链表,如下:
Commit phase
这个阶段会从 completeRoot 函数开始。在执行这个函数之前,会先把
FiberRoot
上的finishedWork
属性置为null
:不同于
render
阶段,commit
阶段的工作总是同步执行的,它可以保证更新HostRoot
的安全性。commit
阶段主要是处理 DOM 更新,以及调用componentDidUpdate
等生命周期方法。这是通过遍历处理render
阶段构建的 effects list 实现的。对于
span
节点和ClickCounter
组件,我们在render
阶段定义了如下的 effects:ClickCounter
的 effectTag 是5
(二进制中是101
),它被解释为调用类组件中的componentDidUpdate
方法。最低有效位被置为 1 表示这个 Fiber 节点在render
阶段已经完成了所有工作。span
的 effectTag 是4
(二进制中是100
),它被解释为需要更新 DOM 上的元素节点。在这个例子中是span
元素,React 会更新元素的textContent
。Applying effects
React 调用 commitRoot 函数来应用这些 effects,它包括了 3 个子函数:
每个子函数都会通过循环遍历 effects list 并检查 effect 的类型。当找到符合当前函数需要处理的类型时,就是应用这个 effect。在我们的例子中,它会更新
span
的文本内容,并调用ClickCounter
的生命周期。第一个函数 commitBeforeMutationLifeCycles 会查找 Snapshot 类型的 effect 并调用
getSnapshotBeforeUpdate
方法。但是,在ClickCounter
组件中我们并没有实现这个方法,所以 React 在render
阶段不会将其加入 effect 中。也就是说,在我们的例子中,这个函数什么也不做。DOM updates
接下来,React 会执行下一个 commitAllHostEffects 函数。这里,React 会将
span
元素的文本内容从0
改为1
。ClickComponet
不需要进行任何处理,因为类组件没有涉及到任何 DOM 更新的操作。这个函数的主要作用是找到类型匹配的 effect 然后执行对应的操作。在我们的例子中我们需要更新
span
元素的文本,因此会进入这里的Update
分支:继续执行
commitWork
函数的话,我们最终会进入到 updateDOMProperties 函数。它会拿到render
阶段添加到 fiber 节点上的updateQueue
数据,然后更新span
元素的textContent
属性:当所有的 DOM 都更新完毕,React 会把
finishedWork
赋值给HostRoot
,也就是把副本(alternate)树赋值给当前树:Calling post mutation lifecycle hooks
最后需要执行的函数是 commitAllLifeCycles。这里 React 会调用所有需要更新后处理的生命周期方法。在
render
阶段,React 为ClickCounter
组件添加了Update
effect。这是commitAllLifeCycles
需要处理的一种 effect,也就是componentDidUpdate
方法:这个函数同时也会更新refs。但这里我们无需用到这个功能。我们只调用了 commitLifeCycles 函数:
你可以看到,如果这是第一次渲染,React 还会调用组件的
componentDidMount
生命周期方法。