前言

在本文中咱们会看到React如何解决state的更新。以及如何构建effects list。咱们会具体介绍render(渲染)阶段以及commit(提交)阶段产生的事件。

咱们会在completeWork函数中看到React如何:

  1. 更新state属性。
  2. 调用render办法并比拟子节点。
  3. 更新React元素的props。

并且在commitRoot函数中React如何:

  1. 更新元素的textContent属性。
  2. 调用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的更新

通过beginWorkupdateClassComponent函数,咱们曾经有了ClickCounter组件的实例,咱们此时进入updateClassInstance。updateClassInstance函数是React解决类组件大部分工作的中央。以下是函数中执行的最重要的操作(按执行顺序排列):

  1. 执行UNSAFE_componentWillReceiveProps生命周期函数
  2. 执行Fiber节点中的updateQueue的更新队列,生成新的的state
  3. 应用新的state,执行getDerivedStateFromProps并获取后果
  4. 执行shouldComponentUpdate判断组件是否须要更新。如果是false,跳过整个render解决,包含此组件以及子组件。如果是true,持续更新。
  5. 执行UNSAFE_componentWillUpdate生命周期函数
  6. 增加effect用来触发componentDidUpdate生命周期函数
  7. 在组件实例上更新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函数。

此时有两个重要的事件须要理解。

  1. React在进行子协调的过程时,创立或者更新了子元素的Fiber节点。子元素由render返回。finishClassComponent办法返回的以后节点的第一个子节点的援用。援用会被调配给nextUnitOfWork变量,而后在workLoop中进行解决。
  2. 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会执行以下的操作:

  1. 筹备DOM更新
  2. 它们增加到span Fiber的updateQueue
  3. 增加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的文章内容较多,所以最初总结下知识点:

  1. 每一个组件都有一个与之关联的updater(更新器)。更新器充当了组件和React core之间的桥梁。这容许setState在ReactDOM,React Native,服务器端渲染和测试用例中有不同的形式实现。
  2. 对于class组件,updater(更新器)是classComponentUpdater
  3. 在更新时,updater(更新器)会在Fiber节点的updateQueue属性中,增加更新队列。
  4. render(渲染)阶段,React会从HostRoot开始遍历Fiber树, 跳过曾经解决过的Fiber节点,直到找到还有work没有实现的Fiber节点。所有工作都是在Fiber节点的备份上进行, 备份存储在Fiber节点的alternate字段上。如果alternate字段还没有创立, React会在解决工作前应用createWorkInProgress创立alternate字段,createWorkInProgress函数会将React元素的状态同步到Fiber节点上。
  5. nextUnitOfWork放弃了对workInProgress tree中一个有工作要解决的Fiber节点的援用。
  6. Fiber节点进入beginWork函数,beginWork函数会依据Fiber节点类型执行绝对应的工作,class组件会被updateClassComponent函数执行 。
  7. 每一个Fiber节点都会执行beginWork函数。通过beginWork函数,组件要么被创立组件实例子,要么只是更新组件实例。
  8. 通过beginWork, updateClassComponent后,进入updateClassInstance,这里是解决类组件大部分work的中央。(上面的操作按程序执行)

    • 执行UNSAFE_componentWillReceiveProps生命周期函数
    • 执行Fiber节点中的updateQueue的更新队列,生成新的的state
    • 应用新的state,执行getDerivedStateFromProps并获取返回后果
    • 执行shouldComponentUpdate判断组件是否须要更新。如果是false,跳过整个render解决,包含此组件以及子组件。如果是true,持续更新。
    • 执行UNSAFE_componentWillUpdate生命周期函数
    • 增加effects用来触发componentDidUpdate生命周期函数(commit阶段才会触发)
    • 在组件实例上更新state和props
  9. render阶段class组件次要做了:调用前置渐变生命周期办法,更新state,定义相干effects。
  10. 实现updateClassInstance完结后,React进入finishClassComponent, 这里React调用组件的render办法,并对子级利用diff算法的中央(子协调的中央)。
  11. 子协调会创立或者更新子元素的Fiber节点,子元素由render办法返回,子元素的属性会被同步到子元素的Fiber节点上。finishClassComponent会返回第一个子元素的Fiber节点,并调配给nextUnitOfWork, 不便之后在workLoop中持续解决(之后会子节点的节点)。
  12. 更新子元素props是在父元素工作中的一部分。
  13. render阶段实现后。React将workInProgress tree(备份节点, render阶段更新的树)调配给FiberRoot节点的finishedWork属性。
  14. commit阶段之前,FiberRootfinishedWork属性设置为null
  15. commit阶段是同步的,是React更新DOM, 以及调用生命周期办法的中央(利用副作用)。

参考

  • In-depth explanation of state and props update in React