前言
在本文中咱们会看到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, 以及调用生命周期办法的中央(利用副作用)。
参考
- In-depth explanation of state and props update in React