本文为意译和整理,若有误导,请放弃阅读。原文。html
这篇文章用一个由parent component和children component组成的例子来说述fiber架构中react将props传递给子组件的处理流程。node
在我先前的文章中 Inside Fiber: in-depth overview of the new reconciliation algorithm in React 提到要想理解更新流程的技术细节,咱们需得具有必定的基础知识。而这部分的基础知识就是篇文章要讲述的内容。react
对于本文所提到的数据结构和概念,我已经在上一篇文章概述过了。这些数据结构和概念主要包括有:git
同时,我也对主要算法进行了宏观上的阐述,也解释过render阶段和commit阶段之间的差别性。若是你尚未阅读过讲述这些东西的文章,我建议你先去阅读。github
我也引入过一个简单demo。这个demo的主要功能是经过点击button来增长界面上的一个数字。 算法
你能够这里去玩玩它。这个demo实现了一个简单的组件。这个组件的render方法返回了两个子组件:button和span。当你点击界面上的按钮的时候,咱们会在click的事件处理器中去更新组件的state。结果是,界面上span元素的文本内容获得更新。数组
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>
]
}
}
复制代码
在这里,我把一个componentDidUpdate
的生命周期函数加入到组件中。这么作,是为了演示React是如何添加effects
和在commit
阶段调用这个方法的。浏览器
在本文中,我会带你看看,React是如何处理state更新和构建effects list的。咱们也对render
和commit
阶段的顶层函数进行简单的讲解。安全
特别地,咱们着重看看completeWork
方法:bash
ClickCounter
组件state中的count
属性。render
方法,获取到children列表,而后执行比对。和commitRoot
方法:
textContent
属性。componentDidUpdate
这个生命周期函数。在深刻这些东西以前,咱们快速地过一遍“当咱们在click事件处理器中调用setState的时候,work是如何被调度”的这一环节。
当咱们点击界面上的button的时候,click事件被触发了,而后React执行咱们做为props传递进去的事件回调。在咱们的demo中,这个事件回调就是简单地经过增长count字段值来更新组件的状态。
class ClickCounter extends React.Component {
...
handleClick() {
this.setState((state) => {
return {count: state.count + 1};
});
}
}
复制代码
每个React组件都有本身的updater
,这个updater
充当着组件与React core通信的桥梁。这种设计,使得多个render(好比:ReactDOM, React Native, server side rendering和testing utilities)去实现本身的setState方法成了可能。
在这篇文章中,咱们单独分析一下updater对象在ReactDOM中的实现。在这个实现中,就用到了Fiber reconciler。具体对于ClickCounter
组件来讲,这个updater对象就是classComponentUpdater。它的职责有:1)把Fiber的实例检索回来; 2)将更新请求入队;3)对work进行调度。
当咱们说“一个更新请求被入队”,其实意思就是把一个setState的callback添加到Fiber node的“updateQueue”队列中去,等待处理。回归到本示例,ClickCounter
组件所对应的Fiber node具体的数据结构:
{
stateNode: new ClickCounter,
type: ClickCounter,
updateQueue: {
baseState: {count: 0}
firstUpdate: {
next: {
payload: (state) => { return {count: state.count + 1} }
}
},
...
},
...
}
复制代码
正如你所看到的那样,updateQueue.firstUpdate.next.payload
引用所指向的那个函数就是咱们传给setState方法的那个callback。它表明着render
阶段第一个须要被处理的“更新请求”。
在我先前的那篇文章关于work loop的那一章节中,我已经解释过nextUnitOfWork
这个全局变量所扮演的角色了。特别地,这一章节说到了这个全局变量指向的是workInProgress
tree上那些有work须要去作的Fiber node。当React遍历整颗Fiber树的时候,就是用这个全局变量来判断是否还有未完成本身的work的Fiber node。
咱们从setState方法已经被调用的地方开始提及。在setState方法被调用以后,React会把咱们传给setState的callback传递ClickCounter
fiber node,也就是说把这个callback添加到fiber node的updateQueue
对象中。而后,就开始调度work。也是从这里开始,React开始进入了render
阶段了。它调用renderRoot这个函数,从最顶层的HostRoot
开始遍历整颗fiber node树。尽管是从最顶层的根节点开始,可是React会掉过那些已经处理过的 fiber node,只会处理那些还有work须要去完成的节点。此时此刻,咱们只有一个fiber node是有work须要去作的。这个node就是ClickCounter
fiber node。
ClickCounter
fiber node的alternate
字段用于保存一个指向[当前fiber node的克隆副本]的引用。这个克隆副本上的work都是已经执行完成的了。这个克隆副本被称为当前fiber node的alternate fiber node
。若是alternate fiber node
尚未被建立的话,那么React就会在处理更新请求以前使用createWorkInProgress函数去完成复制工做。如今,咱们假设变量nextUnitOfWork
保存着指向当前fiber node的alternate fiber node
的引用。
首先,咱们的fiber node将会被传递到beginWork 函数里面。
由于这个函数会在fiber node tree上的每个节点调用。因此,若是你想调试
render
阶段,这是一个打断点的好地方。我常常这么干,经过检测fiber node的type值来肯定当前节点是不是我要跟进的那个。
beginWork
函数基本上就是一个大的switch语句。在这个switch语句,beginWork
根据workInProgress的tag值来计算初当前fiber node所须要完成的work的类型。而后,执行相应的函数去执行这个work。在咱们的demo中,由于ClickCounter
是一个class component,因此,咱们会执行如下的分支语句:
function beginWork(current$$1, workInProgress, ...) {
...
switch (workInProgress.tag) {
...
case FunctionalComponent: {...}
case ClassComponent:
{
...
return updateClassComponent(current$$1, workInProgress, ...);
}
case HostComponent: {...}
case ...
}
复制代码
那么,咱们会进入updateClassComponent函数中。取决于当前:1)是不是组件的首次渲染:2)是不是work正在被恢复执行;3)是不是一次React更新,React会干两件事情:
function updateClassComponent(current, workInProgress, Component, ...) {
...
const instance = workInProgress.stateNode;
let shouldUpdate;
if (instance === null) {
...
// In the initial pass we might need to construct the instance.
constructClassInstance(workInProgress, Component, ...);
mountClassInstance(workInProgress, Component, ...);
shouldUpdate = true;
} else if (current === null) {
// In a resume, we will already have an instance we can reuse.
shouldUpdate = resumeMountClassInstance(workInProgress, Component, ...);
} else {
shouldUpdate = updateClassInstance(current, workInProgress, ...);
}
return finishClassComponent(current, workInProgress, Component, shouldUpdate, ...);
}
复制代码
咱们已经为ClickCounter
建立过一个实例了,因此,咱们的执行将会进入updateClassInstance
方法。在这个方法中,React执行了class component绝大部分的work。如下是这个方法执行的最重要的操做(罗列的顺序也是代码执行的顺序):
UNSAFE_componentWillReceiveProps
生命周期函数(已弃用);updateQueue
中的更新请求和生成一个新的state值;getDerivedStateFromProps
,并获取调用结果。shouldComponentUpdate
来确保一个组件是否真的想要更新。若是调用返回值为false的话,那么React将会跳过整个渲染流程包括调用组件实例和它的子组件实例的render方法。不然的话,正常走更新流程。UNSAFE_componentWillUpdate
生命周期函数(已弃用);componentDidUpdate
添加成一个effect。虽然,“调用
componentDidUpdate
”这个effect是在render
阶段添加的,可是这个方法的实际执行是在接下来的commit
阶段。
state和props值的更新应该是在render方法调用前的。由于render的返回值是须要依赖最新的state和props值(译者注:这也是指出了一个事实,即react组件更新的本质就是用最新的state和props值去调用组件实例的render方法)。若是咱们不这么干的话,那么render方法的每一次调用的返回值都是同样的。
下面是updateClassInstance
方法的精简版:
function updateClassInstance(current, workInProgress, ctor, newProps, ...) {
const instance = workInProgress.stateNode;
const oldProps = workInProgress.memoizedProps;
instance.props = oldProps;
if (oldProps !== newProps) {
callComponentWillReceiveProps(workInProgress, instance, newProps, ...);
}
let updateQueue = workInProgress.updateQueue;
if (updateQueue !== null) {
processUpdateQueue(workInProgress, updateQueue, ...);
newState = workInProgress.memoizedState;
}
applyDerivedStateFromProps(workInProgress, ...);
newState = workInProgress.memoizedState;
const shouldUpdate = checkShouldComponentUpdate(workInProgress, ctor, ...);
if (shouldUpdate) {
instance.componentWillUpdate(newProps, newState, nextContext);
workInProgress.effectTag |= Update;
workInProgress.effectTag |= Snapshot;
}
instance.props = newProps;
instance.state = newState;
return shouldUpdate;
}
复制代码
我已经把一些比较次要的代码移除掉了。举个例子,在调用生命周期函数和添加effect并触发它以前,React会用typeof操做符去检查这个组件是否实现了某个方法。下面的代码中,React会在添加effect以前检查componentDidUpdate
方法是不是一个function:
if (typeof instance.componentDidUpdate === 'function') {
workInProgress.effectTag |= Update;
}
复制代码
到了这里,咱们已经知道在render
阶段,ClickCounter
fiber node须要执行哪些操做了。下面,咱们来看看,这些操做是如何改变fiber node上的相关值的。当React开始执行work的时候,ClickCounter
组件所对应的fiber node长这样的:
{
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) => {…}
}
},
...
}
}
复制代码
当work执行完毕,ClickCounter
组件所对应的fiber node已经长成这样的:
{
effectTag: 4,
elementType: class ClickCounter,
firstEffect: null,
memoizedState: {count: 1},
type: class ClickCounter,
stateNode: {
state: {count: 1}
},
updateQueue: {
baseState: {count: 1},
firstUpdate: null,
...
}
}
复制代码
仔细观察一下连个fiber node属性值之间的差别。咱们会发现,在处理完更新请求后,memoizedState和baseState中的count字段的属性值已经变为1了。与此同时,React也把ClickCounter
的组件实例的状态也更新了。
当前,咱们在updateQueue中已经没有更新请求了,因此firstUpdate
的值为null。还有很重要的一点,咱们的effectTag
字段的值已经从0变为4了。4用二进制表示就是100
,而这就是update
这个side-effect的tag值:
export const Update = 0b00000000100;
复制代码
下面作个小总结。当React在ClickCounter
fiber node上执行work的时候,React要作的事有:
当上面提到的小总结的东西完成后,React执行将会进入finishClassComponent
。在这个函数里面,React将会调用组件实例的render
方法,而后在它的子组件实例(正是render方法返回的东西)上应用diff算法。在这篇文章里面有一个关于diff算法高质量的归纳:
当对比中的两个react DOM element(译者注:本质上就是react element,可是type的值是DOM类型的字符串)具体相同的type的时候,React会查看二者的attribute的差别性,保留底层所对应的DOM node对象,只是更新那些须要改变的attribute。
若是咱们再深究一点的话,那么,咱们会了解到其实对比是react element所对应的fiber node。在本文中,我不会讨论太多细节,由于这里面的处理流程仍是挺复杂的。我将会在一个单独的文章上专门来说述child reconciliation的处理流程。
若是你着急去了解child reconciliation细节的话,那么你能够查看这个reconcileChildrenArray函数。由于在咱们这个demo中,
ClickCounter
的render方法返回的是一个react element组成的数组。
当前,有两件重要的事情须要咱们去理解。第一件是,随着child reconciliation流程的执行,React会为从render方法中返回的child react element建立或者更新对应的fiber node。finishClassComponent函数会返回当前fiber node第一个child fiber node的引用。这个引用将会赋值给nextUnitOfWork
,而且会在work loop的下一个循环中使用到;第二件事是,React把对子fiber node 的props的更新看成父fiber node的work的一部分。为了达成这事,React会使用从render方法返回的react element身上的数据。
举个例子,在React对ClickCounter
fiber node 的children进行reconcile以前,span元素所对应的fiber node是长这样的:
{
stateNode: new HTMLSpanElement,
type: "span",
key: "2",
memoizedProps: {children: 0},
pendingProps: {children: 0},
...
}
复制代码
正如你所见的那样,memoizedProps
和pendingProps
中的children属性值都是0 。而下面,就是调用render方法后返回的span元素所对应的react element:
{
$$typeof: Symbol(react.element)
key: "2"
props: {children: 1}
ref: null
type: "span"
}
复制代码
正如你所见的那样,fiber node中的props与返回的react element中的props是不一样的。在createWorkInProgress函数中,这种不一样性会应用 在alternate fiber node的建立过程当中。React就是从react element上拷贝已经更新的props到alternate fiber node上的。
当React对ClickCounter
组件的children完成了reconcile以后,span元素所对应的fiber node的pendingProps字段的值将获得更新。该字段值将会跟span元素所对应的react element的props值保持一致:
{
stateNode: new HTMLSpanElement,
type: "span",
key: "2",
memoizedProps: {children: 0},
pendingProps: {children: 1},
...
}
复制代码
稍后,React会span元素所对应的fiber node执行work,它会将它们复制到memoizedProps
上,并向DOM更新上添加effect(add effect to DOM update)。
到此为止,咱们已经讲完了ClickCounter
fiber node在render阶段所须要执行的全部的work了。由于button组件是ClickCounter
组件的第一个子元素,因此,它所对应的fiber node将会被赋值给nextUnitOfWork
变量。由于这个fiber node没有任何work须要去作的。因此,React会移步到它的sibling-span元素所对应的fiber node。根据这里所描述的算法能够得知,以上过程发生在completeUnitOfWork
函数里面。
因此,nextUnitOfWork
变量如今指向span元素所对应的fiber node(后面简称为“span fiber node”)的alternate fiber node。React对span fiber node的更新处理流程就是从这里开始。跟ClickCounter
fiber node的处理流程是同样的,咱们都是从beginWork函数开始。
由于span节点属于HostComponent
类型的,因此,这一次,咱们会进入HostComponent的分支:
function beginWork(current$$1, workInProgress, ...) {
...
switch (workInProgress.tag) {
case FunctionalComponent: {...}
case ClassComponent: {...}
case HostComponent:
return updateHostComponent(current, workInProgress, ...);
case ...
}
复制代码
最终,咱们会进入updateHostComponent这个函数。往上,你能够看到咱们上面在分析ClickCounter
fiber node时候的所提到的updateClassComponent
,针对functional component,React会执行updateFunctionComponent等等。你能够在ReactFiberBeginWork.js文件中找到全部的这些函数的实现代码。
在咱们的demo中,由于span节点的子节点太过简单了,因此在updateHostComponent函数中,没啥过重要的事情发生。
一旦beginWork执行完毕,当前fiber node就会被传递到completeWork中去。在本示例中,这个fiber node就是span fiber node。在此以前,React须要更新span fiber node上的memoizedProps
字段值。你可能还记得,当React对ClickCounter
组件的子组件进行reconcile的时候,它已经更新span fiber node上的pendingProps字段:
{
stateNode: new HTMLSpanElement,
type: "span",
key: "2",
memoizedProps: {children: 0},
pendingProps: {children: 1},
...
}
复制代码
因此,一旦beginWork函数在span fiber node上调用完毕的话,那么React会更新memoizedProps字段值,使得它与pendingProps字段值保持一致:
function performUnitOfWork(workInProgress) {
...
next = beginWork(current$$1, workInProgress, nextRenderExpirationTime);
workInProgress.memoizedProps = workInProgress.pendingProps;
...
}
复制代码
执行完beginWork
函数后,React就会执行completeWork
函数。这个函数的实现基本上就是一个大大的switch语句。这跟以前所提到的beginWork里面的switch语句差很少:
function completeWork(current, workInProgress, ...) {
...
switch (workInProgress.tag) {
case FunctionComponent: {...}
case ClassComponent: {...}
case HostComponent: {
...
updateHostComponent(current, workInProgress, ...);
}
case ...
}
}
复制代码
由于咱们的span fiber node(所对应的react element)是HostComponent
,因此,咱们会进入到updateHostComponent函数里面。在这个函数里面,React基本上就作了如下的三件事情:
在执行这行操做以前,span fiber node长这样的:
{
stateNode: new HTMLSpanElement,
type: "span",
effectTag: 0
updateQueue: null
...
}
复制代码
当上面的work执行完成后,span fiber node长这样:
{
stateNode: new HTMLSpanElement,
type: "span",
effectTag: 4,
updateQueue: ["children", "1"],
...
}
复制代码
请注意二者在effectTag和updateQueue字段值上的不一样。对于effectTag的值来讲,它再也不是0,而是4。用二进制表示就是100
,而第三位就是update这 种side-effect所对应的二进制位。现在该位置为1,则说明span fiber node后面所须要执行的side-effect就是update
。在接下来的commit阶段,对于span fiber node来讲,这也是React惟一须要帮它完成的任务了。而updateQueue字段值保存的是用于update的数据。
一旦React处理完ClickCounter
fiber node和它的子fiber node们,那么render阶段算是结束了。React会把产出的alternate fiber node树赋值给FiberRoot对象的finishedWork属性。这颗新的alternate fiber node树包含了须要被flush到屏幕的东西。它会在render阶段以后立刻被处理或者稍后在浏览器分配给React的,空闲的时间里面执行。
在咱们给出的示例中,由于span fiber node和ClickCounter fiber node是有side effect的。React将会给span fiber node添加一个link,让它指向HostFiber的firstEffect属性
在函数compliteUnitWork中,react完成了effect list的构建。下面就是本示例中,带有effect的fiber node树。在这棵树上,有着两个effect:1)更新span节点的文本内容;2)调用ClickCounter
组件的生命周期函数:
而下面是由具备effect的fiber node组成的线性列表:
这个阶段以completeRoot函数开始。在继续往下走以前,它首先将FiberRoot的finishedWork属性值置为null:
root.finishedWork = null;
复制代码
不像render阶段,commit阶段是同步执行的。因此,它能很安全地更新HostRoot,以此来指示commit工做已经开始了。
commit阶段是React进行DOM操做和调用post-mutation生命周期方法componentDidUpate的地方。为了实现上面这些目标,React会遍历render阶段所产出的effect list,并应用相应的effect。
就本示例而言,咱们在render阶段事后,咱们有如下几个effect:
{ type: ClickCounter, effectTag: 5 }
{ type: 'span', effectTag: 4 }
复制代码
ClickCounter
fiber node的effect tag为5,用二进制表示就是“101”。它对应的work是update
。而对于class component而言,这个work会被“翻译为”componentDidUpdate这个生命周期方法。在二进制“101”中,最低位为“1”,表明着当前这个fiber node的全部work都在render阶段执行完毕了。
span fiber node的effect tag值是4,用二进制表示是“100”。这个编号所表明的work是“update”,由于当前的span fiber node对应的是host component类型的。这个“update”work更具体点来讲就是“DOM更新”。回归到本示例,“DOM更新”更具体点是指“更新span元素的textContent属性”。
让咱们一块儿来看看,React是如何应用这些effect的。函数commitRoot就是用来应用effect的。它由三个子函数组成:
function commitRoot(root, finishedWork) {
commitBeforeMutationLifecycles()
commitAllHostEffects();
root.current = finishedWork;
commitAllLifeCycles();
}
复制代码
这三个子函数都实现对effect list的遍历,而且在遍历过程当中去检查effect的类型。若是它们发现当前的这个effect跟它们函数的职责相关的,那么就会应用这个effect。在咱们的示例中,具体点讲就是在ClickCounter
组件上调用componentDidUpdate
这个生命周期方法和更新span元素的文本内容。
第一个子函数commitBeforeMutationLifeCycles 会查找snapshot
类型的effect,并调用getSnapshotBeforeUpdate方法。由于在ClickCouner
组件身上,咱们并无实现这个方法,因此React并无在render阶段把这个effect添加到该组件对应的fiber node身上。因此,在咱们这个示例中,这个子函数啥事都没作。
接下来,React会移步到commitAllHostEffects函数上面来。就是在这个函数里面,React完成了将span元素的文本内容从“0”更新到“1”。这个函数几乎跟ClickCounter
这个fiber node没有关系。由于这个fiber node对应的是class component,而class componnet是没有任何的直接的DOM更新方面的需求的。
这个函数的大致框架是对不一样类型的effect执行不一样操做。在咱们这个示例中,咱们须要更新span元素的文本内容,因此咱们是要走Update这条分支的:
function updateHostEffects() {
switch (primaryEffectTag) {
case Placement: {...}
case PlacementAndUpdate: {...}
case Update:
{
var current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Deletion: {...}
}
}
复制代码
顺着commitWork
一路走下去,咱们最终会进入updateDOMProperties函数里面。在这个函数里面,它使用了咱们在render阶段添加到fiber node的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更新]这个effect被应用后,React将finishedWork树赋值给HostRoot。它把alternate tree设置为current tree:
root.current = finishedWork;
复制代码
咱们剩下最后一个commitAllLifecycles要讲了。在这个函数里面,React调用了全部的post-mutational 生命周期方法。在render阶段,React往ClickCounter
组件身上添加了一个叫“update”的effect。这个effect就是本函数所要查找的effect,一旦找到以后,React就会调用componentDidUpdate方法:
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;
}
}
复制代码
这个函数也会更新refs,可是由于咱们这个示例中并无使用到这个特性。因此相应的那部分代码(指commitAttachRef(nextEffect);
)就不会被执行。对componentDidUpdate方法的调用是发生在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这个生命周期方法的地方。不过这个调用时机是在组件的首次挂载的过程当中而已。