[译] 深刻解析React中state和props更新

原文连接:medium.com/react-in-de…javascript

到react fiber内部一探究竟html

在我以前文章Fiber内部:深度概述React新协调算法中,我铺设了基础内容,用于理解我这篇文章讲解的更新处理的技术细节。java

我已经概述过将在这篇文章中用到的主要数据结构和概念,特别是Fiber节点、当前和工做过程树、反作用和做用列表,我也对主要的算法提供过大体的说明,且解释过**rendercommit**阶段的不一样。若是你尚未读过,那我建议你从那里仍是react

我也介绍过一个button的简单应用,在屏幕上渲染一个递增的树:git

你能够在这里运行它,它实现了一个简单的组件,经过**render方法返回两个子元素buttonspan。当你点击button时,组件的状态就会在处理方法中更新,span**元素的文本更新结果:github

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如何在commit**阶段添加做用(effects)来调用这个方法。算法

这篇文章中,我想向你展现React如何处理状态更新以及构建做用列表,咱们将带你去看看**rendercommit**阶段中大体方法都作了些什么。数组

特别的是,咱们将看到React在completeWork中是如何:浏览器

  • 更新**ClickCounterstate中的count**属性。
  • 调用**render**方法来获取子节点列表,以及执行比较。
  • 更新**span**元素的props

还有,React在commitRoot安全

  • 更新**span元素的textCount**属性。
  • 调用**componentDidUpdate**生命周期方法。

在这以前,咱们先看看,当咱们在click处理方法中调用**setState**时,工做(就是指的work loop中的work啦)如何如何调用的。

注意,你须要知道这里的一块儿来用React,这篇文章是关于React如何内部工做的。

调度更新(Scheduling updates)

当咱们点击button时,**click**事件被触发,React执行经过props传给button的回调方法,在咱们的应用中,它简单的增长计数器,并更新状态:

class ClickCounter extends React.Component {
    ...
    handleClick() {
        this.setState((state) => {
            return {count: state.count + 1};
        });
    }
}   
复制代码

每个React组件都有相关联**updater,它扮演组件与React内核的桥,这使得setState**在ReactDOM、React Native、服务端渲染以及测试工具中有不一样的实现。

这篇文章中,咱们将看看ReactDOM中updater对象的实现,它使用了Fiber协调器。对于**ClickCounter**组件,它是classComponentUpdater,它负责获取Fiber实例、队列化更新以及调度工做。

当更新队列化了,它们就在Fiber节点上添加一个待处理的更新队列,在咱们的例子中,负责**ClickCounter**组件的Fiber节点有以下结构:

{
    stateNode: new ClickCounter,
    type: ClickCounter,
    updateQueue: {
         baseState: {count: 0}
         firstUpdate: {
             next: {
                 payload: (state) => { return {count: state.count + 1} }
             }
         },
         ...
     },
     ...
}
复制代码

正如你所见,**updateQueue.firstUpdate.next.payload中的方法就是咱们在ClickCounter组件中传递给setState的回调,它表示在render**阶段须要处理的第一个更新。

处理ClickCounter Fiber节点的更新

个人前一篇文章的工做循环章节解释了**nextUnitOfWork全局变量的角色,特别是,它持有了来自还有工做待作的workInProgress**树中的Fiber节点。当React遍历Fiber树时,使用它来知道是否要有其余未完成工做的Fiber节点。

咱们从假定**setState方法被调开始,React在ClickCounter上添加setState的回调,且调用工做,React进入render阶段。它使用renderRoot方法从顶层HostRoot开始遍历,而后它会调用以及处理过的fiber节点,直到发现尚未完成工做的节点,在这一点上,这里只有一个fiber节点有工做作,就是ClickCounter**Fiber节点。

全部工做都在fiber的副本上执行,这个副本保存在**alternate字段中,若是这个alternate节点尚未建立,React会在处理更新以前在createWorkInProgress方法中建立这个副本。咱们来假定这个nextUnitOfWork遍历就持有这个副本ClickCounter**Fiber节点的引用。

beginWork

首先,咱们Fiber进入beginWork方法。

由于这个方法在树中的每一个fiber节点上都会执行,因此若是你想在**render**阶段debug,那这是很好断点位置,我常常这样来检查Fiber节点类型,以便于肯定我须要的那个节点。

**beginWork基本上是一个大的switch语句,根据tag来肯定每一个Fiber须要作的工做类型,而后执行各自的方法,在咱们的CountClicks**例子中,它是个类组件,因此这部分被执行:

function beginWork(current$$1, workInProgress, ...) {
    ...
    switch (workInProgress.tag) {
        ...
        case FunctionalComponent: {...}
        case ClassComponent:
        {
            ...
            return updateClassComponent(current$$1, workInProgress, ...);
        }
        case HostComponent: {...}
        case ...
}
复制代码

咱们进入updateClassComponent方法,依赖于它是首次渲染、工做恢复继续(work不是能够异步打断的嘛),或者只是更新,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'll already have an instance we can reuse.
        shouldUpdate = resumeMountClassInstance(workInProgress, Component, ...);
    } else {
        shouldUpdate = updateClassInstance(current, workInProgress, ...);
    }
    return finishClassComponent(current, workInProgress, Component, shouldUpdate, ...);
}
复制代码

处理ClickConter Fiber的更新

咱们已经有**ClickCounter**组件的实例,因此咱们进入updateClassInstance,这是React处理类组件大部分工做的地方,方法中按顺序有最重要的几个操做:

  • 调用 UNSAFE_componentWillReceiveProps 钩子 (弃用)
  • 执行 **updateQueue**中的更新,并生成新的state
  • 使用新的state调用**getDerivedStateFromProps**,并得到结果
  • 调用**shouldComponentUpdate确保组件是否须要更新,若是不,则跳过整个render处理,包括该组件和其子组件的render**调用,反之则用更新处理。
  • 调用**UNSAFE_componentWillUpdate** (弃用)
  • 添加一个做用(effect)来触发**componentDidUpdate**生命周期钩子

尽管调用**componentDidUpdate的做用在render阶段添加,可是这个方法将在commit**阶段被执行

  • 在组件实例上更新**stateprops**

stateprops应该在组件实例的render方法调用以前被更新,由于render方法的输出一般依赖于stateprops,若是咱们不这样作,那它将老是返回一样的结果。

这是这个方法的简单版本:

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;
}
复制代码

在上面的代码片断中我已经移除掉一些辅助代码,对于实例,在调用生命周期方法或者添加触发它们的做用前,React使用typeof操做符检测组件是否实现了这个方法。例如,这里即是React检测**componentDidUpdate**,在它这个做用添加以前:

if (typeof instance.componentDidUpdate === 'function') {
    workInProgress.effectTag |= Update;
}
复制代码

好,如今,我知道了**ClickCounter在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,
        ...
    }
}
复制代码

花点时间观察一下属性值的不一样

在更新执行以后,count属性的值在memoizedStateupdateQueue中的baseState上变成1,React也更新了**ClickCounter**组件实例中的state。

此刻,咱们在队列中不在有更新,全部**firstUpdatenull,且重要的是,咱们修改了effectTag属性的值,它不在是0,而是4,二进制中为100,这表明第三位被设,而这一位表明Update**的反作用tag(side-effect tag)

export const Update = 0b00000000100;
复制代码

综述,**ClickCounter**Fiber节点的工做是,调用前置突变生命周期方法,更新state以及定义相关反作用。

ClickCounter Fiber的子协调

一旦那些完成,React进入finishClassComponent,这个方法中,React调用组件实例**render**方法,且对组件返回的孩子执行diff算法,这个文档中有大体概述,这是相关的一部分:

当比较两个相同类型的React DOM元素时,React观察二者的属性,保留DOM节点中一致的,且只更新变化的属性。

而后若是咱们再深刻的话,咱们能够知道它的确是比较Fiber节点和React元素,可是我如今就先不太详细的说明了,由于这个处理至关细致,我将会针对子协调单独的写文章分析。

若是你本身很好奇想知道这个细节,能够查阅reconcileChildrenArray方法,由于在咱们的例子中,**render**方法返回React元素数组。

此刻,有两个重要事情须要理解,首先,当React进行子协调处理时,它建立或更新了子React元素的Fiber节点,这些子元素有**render方法返回,finishClassComponent返回了当前Fiber节点的第一个孩子的引用,它将会赋值给nextUnitOfWork,便于在工做循环中以后处理;其次,React更新孩子的props是其父级上执行工做的一部分,因此为了作这个,它要使用render**方法返回的react元素上的数据。

例如,这里是**span元素相关的Fiber节点在React协调ClickCounter**fiber以前的样子:

{
    stateNode: new HTMLSpanElement,
    type: "span",
    key: "2",
    memoizedProps: {children: 0},
    pendingProps: {children: 0},
    ...
}
复制代码

正如你所见,memoizedPropspendingPropschildren属性值都是0,这是**span元素的render**方法返回的React元素结构:

{
    $$typeof: Symbol(react.element)
    key: "2"
    props: {children: 1}
    ref: null
    type: "span"
}
复制代码

如你所见,Fiber节点和返回的React元素的props有点不一样,在建立fiber节点副本的createWorkInProgress方法中,React从React元素中拷贝了更新的属性到Fiber节点

因此,在React完成**ClickCounter组件上的子协调后,spanFiber节点的pendingProps将会更新,它们将会匹配span**React元素中的值:

{
    stateNode: new HTMLSpanElement,
    type: "span",
    key: "2",
    memoizedProps: {children: 0},
    pendingProps: {children: 1},
    ...
}
复制代码

而后,当React将要执行**spanFiber节点上的工做,它将拷贝它们到memoizedProps**,并添加做用(effects)来更新DOM。

嗯,这就是在render阶段中,React在**ClickCounterFiber节点上执行的全部工做,由于Button是ClickCounter组件上的第一孩子,它将赋值给nextUnitOfWork变量,因为无事可作,因此React将会转移到它的兄弟spanFiber节点上,根据这里描述的算法,这发生在completeUnitOfWork**方法中。

Span Fiber的更新处理

因此,**nextUnitOfWork变量如今指向span副本,且React在它上面仍是工做,相似于在ClickCounte**上的步骤,咱们开始于beginWork方法。

因为咱们的**span节点是HostComponent**类型,此次在switch语句中,React执行这个部分:

function beginWork(current$$1, workInProgress, ...) {
    ...
    switch (workInProgress.tag) {
        case FunctionalComponent: {...}
        case ClassComponent: {...}
        case HostComponent:
          return updateHostComponent(current, workInProgress, ...);
        case ...
}
复制代码

走到updateHostComponent方法,你能够对比看类组件调用的**updateClassComponent方法,方法组件的updateFunctionComponent**,以及其余,你能在ReactFiberBeginWork.js文件中找到全部这些方法。

span fiber的子协调

在咱们的例子中,在**span节点在updateHostComponent**没有什么重要事情发生。

span fiber节点的完成工做

一旦**beginWork完成,该节点就进入completeWork方法,可是在此以前,React须要更新span fiber上的memoizedProps,你可能记得,当ClickCounter组件上子协调时,React更新spanFiber节点上的pendingProps**:

{
    stateNode: new HTMLSpanElement,
    type: "span",
    key: "2",
    memoizedProps: {children: 0},
    pendingProps: {children: 1},
    ...
}
复制代码

因此,一旦**spanFiber上的beginWork完成,React就是更新pendingPropsmemoizedProps**上:

function performUnitOfWork(workInProgress) {
    ...
    next = beginWork(current$$1, workInProgress, nextRenderExpirationTime);
    workInProgress.memoizedProps = workInProgress.pendingProps;
    ...
}
复制代码

而后,它调用**completeWork方法,这个基本上是一个大的switch语句,相似于咱们在beginWork**中看到的:

function completeWork(current, workInProgress, ...) {
    ...
    switch (workInProgress.tag) {
        case FunctionComponent: {...}
        case ClassComponent: {...}
        case HostComponent: {
            ...
            updateHostComponent(current, workInProgress, ...);
        }
        case ...
    }
}
复制代码

由于咱们的**spanFiber节点是HostComponent**,因此它执行updateHostComponent方法,在这个方法中,React基本上以下操做:

  • 准备DOM更新
  • 把它们添加到**spanfiber的updateQueue**
  • 添加更新DOM的做用

在操做执行以前,**span**Fiber节点看起来像:

{
    stateNode: new HTMLSpanElement,
    type: "span",
    effectTag: 0
    updateQueue: null
    ...
}
复制代码

当工做完成以后,它看起来像:

{
    stateNode: new HTMLSpanElement,
    type: "span",
    effectTag: 4,
    updateQueue: ["children", "1"],
    ...
}
复制代码

注意,effectTagupdateQueue字段的不一样,它再也不是0,而是**4,二进制为100,这表明第三位被设,第三位表明着Update的反作用tag,这是React在接下来的commit阶段惟一须要作的工做,而updateQueue**字段持有的负载(payload)将会在更新时用到。

一旦,React处理完**ClickCounter和它们的孩子,它就完成了render阶段,它如今就能把完成成的副本(或者叫替代-alternate)树赋值给FiberRoot上的finishedWork属性。这是一颗新的须要刷新在屏幕上的树,它能够在render**阶段以后当即处理,或者挂起等浏览器给React空闲时间。

做用列表(Effects list)

在咱们的例子中,由于span节点和ClickCounter组件都有反作用,React会把HostFiber上的firstEffect指向spanFiber节点。

React在compliteUnitOfWork方法中构建做用列表,这里是带有做用的Fiber树,这些做用是更新**span节点文本,调用ClickCounter**的钩子:

这里是做用节点的线性列表:


Commit 阶段

这个阶段开始于completeRoot方法,在它作任何工做以前,它把**FiberRoot上的finishedWork属性设为null**:

root.finishedWork = null;

不像**render阶段,commit阶段老是同步的,因此它能够安全的更新HostRoot**来指示提交工做开始了。

**commit阶段就是React更新DOM以及调动后置突变生命周期方法componentDidUpdate的地方,为了这样,它迭代在render**阶段建立的做用列表,并应用它。

咱们在**render阶段中,对spanClickCounter**节点有以下做用:

{ type: ClickCounter, effectTag: 5 }
{ type: 'span', effectTag: 4 }
复制代码

ClickCounter的做用标签值是5或者二进制的101,定义的**更新工做被认为是调用类组件的componentDidUpdate生命周期方法。最低位也被设值,它表明这个Fiber节点在render**阶段的全部工做都已完成。

span的做用标签是4或者二进制100,定义的**更新工做是host组件的DOM更新,在咱们例子中的span元素,React将须要更新元素的textContent**。

应用做用(Applying effects)

咱们来看React是如何应用这些做用的,commitRoot方法,用于应用这些做用,有三个子方法组成:

function commitRoot(root, finishedWork) {
    commitBeforeMutationLifecycles()
    commitAllHostEffects();
    root.current = finishedWork;
    commitAllLifeCycles();
}
复制代码

每一个子方法都会用循环来迭代做用列表,并检查其中做用类型,当找到有关方面目的的做用时,就应用它。在咱们的例子中,它会调用**ClickCounter组件的componentDidUpdate生命周期方法,以及更新span**元素上的文本内容。

第一个放commitBeforeMutationLifeCycles寻找**Snapshot做用,且调用getSnapshotBeforeUpdate方法,可是,由于咱们在ClickCounter组件中没有实现这个方法,因此React不会在render**阶段添加这个做用,因此在咱们的例子中,这个方法啥也没作。

DOM更新

接着,React执行到commitAllHostEffects方法,这里,React就会把**span元素的文本内容从0修改到1,而ClickCounter**fiber上啥也不作,由于类组件的节点没有任何DOM更新。

这个方法大体是,选择正确做用类型,并应用相关的操做。在个人例子中,咱们须要更新**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节点上的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赋值给HostRoot**,即把替换(alternate)树设置为当前树:

root.current = finishedWork;

调用后置突变生命周期钩子

最后一个方法是commitAllLifecycles方法,这里React会调用后置突变生命周期方法。在**render阶段,React在ClickCounter组件上添加Update做用,这是commitAllLifecycles方法寻找的做用之一,而后调用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,可是咱们没有使用这个功能,因此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**方法。

额……讲完啦。

相关文章
相关标签/搜索