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

在个人上篇文章 Inside Fiber: 深刻了解React新协调算法中介绍了理解更新过程细节的所需的基础知识,我将在本文中描述这个更新过程。html

我已经概述了将在本文中使用的主要数据结构和概念,特别是Fiber节点,currentwork-in-progress树,反作用(side-effects)以及effects链表(effects list)。我也提供了主要算法的高级概述和render阶段与commit阶段的差别。若是你尚未阅读过它,我推荐你从那儿开始。react

我还向你介绍了带有一个按钮的示例程序,这个按钮的功能就是简单的增长数字。git

你能够在这查看在线代码。它的实现很简单,就是一个render函数中返回buttonspan元素的类组件。当你点击按钮的时候,在点击事件的处理函数中更新组件的state。结果就是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如何添加effects并在commit阶段调用这个方法。在本文中,我想向你展现React是如何处理状态更新和建立effects list的。咱们能够看到render阶段和commit阶段的高级函数中发生了什么。算法

尤为是在React的completeWork函数中:数组

  • 更新ClickCounterstate中的count属性
  • 调用render方法获取子元素列表并比较
  • 更新span元素的props

以及,在React的commitRoot 函数中:浏览器

  • 更新span元素的文本内容属性
  • 调用componentDidUpdate生命周期方法

可是在那以前,咱们先快速看看当咱们在点击处理函数中调用setState时工做是如何调度的。安全

请注意,你无需了解这些来使用React。本文是关于React内部是如何运做的。markdown

调度更新

当咱们点击按钮时,click事件被触发,React执行传递给按钮props的回调。在咱们的程序中,它只是简单的增长计数器和更新state数据结构

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

每一个组件都有相应的updater,它做为组件和React核心之间的桥梁。这容许setState在ReactDOM,React Native,服务端渲染和测试程序中是不一样的实现。(译注:从源码能够看出,setState内部是调用updater.enqueueSetState,这样在不一样平台,咱们均可以调用setState来更新页面)

本文中,咱们关注ReactDOM中实现的updater对象,它使用Fiber协调器。对于ClickCounter组件,它是classComponentUpdater。它负责获取Fiber实例,为更新入列,以及调度work。

当更新排队时,它们基本上只是添加到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节点的更新

我上篇文章中的work循环部分中解释了全局变量nextUnitOfWork的角色。尤为是,这个变量保存workInProgress树中有work待作的Fiber节点的引用。当React遍历树的Fiber时,它使用这个变量知道是否存在其余有未完成work的Fiber节点。

咱们假定setState方法已经被调用。 React将setState中的回调添加到ClickCounterfiber节点的updateQueue中,而后调度work。React进入render阶段。它使用renderRoot函数从最顶层HostRootFiber节点开始遍历。然而,它会跳过已经处理过得Fiber节点直到遇到有未完成work的节点。基于这点,只有一个节点有work待作。它就是ClickCounterFiber节点。

全部的work都是基于保存在Fiber节点的alternate字段的克隆副本执行的。若是alternate节点还未建立,React在处理更新前调用createWorkInProgress函数建立副本。咱们假设nextUnitOfWork变量保存代替ClickCounterFiber节点的引用。

beginWork

首先, 咱们的Fiber进入beginWork函数。

由于这个函数对树中每一个节点执行,因此若是你想调试render阶段,它是放置断点的好地方。 我常常这样作,还有检查Fiber节点的type来肯定我须要的节点。

beginWork函数大致上是个大的switch语句,经过tag肯定Fiber节点须要完成的work的类型,而后执行相应的函数来执行work。在这个例子中,CountClicks是类组件,因此会走这个分支:

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

咱们进入updateClassComponent函数。取决于它是首次渲染、恢复work仍是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'll already have an instance we can reuse.
        shouldUpdate = resumeMountClassInstance(workInProgress, Component, ...);
    } else {
        shouldUpdate = updateClassInstance(current, workInProgress, ...);
    }
    return finishClassComponent(current, workInProgress, Component, shouldUpdate, ...);
}
复制代码

处理ClickCounter Fiber更新

咱们已经有了ClickCounter组件实例,因此咱们进入updateClassInstance。这是React为类组件执行大部分work的地方。如下是在这个函数中按顺序执行的最重要的操做:

  • 调用UNSAFE_componentWillReceiveProps()钩子(已废弃)
  • 处理updateQueue中的更新以及生成新state
  • 使用新state调用getDerivedStateFromProps并获得结果
  • 调用shouldComponentUpdate肯定组件是否须要更新;若是返回结果为false,跳过整个渲染过程,包括在该组件和它的子组件上调用render;不然继续更新
  • 调用UNSAFE_componentWillUpdate(已废弃)
  • 添加一个effect来触发componentDidUpdate生命周期钩子

尽管调用componentDidUpdate的effect是在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;
}
复制代码

上面代码片断中我删除了一些辅助代码。对于实例,调用生命周期方法或添加effects来触发它们前,React使用typeof操做符检查组件是否实现了这些方法。好比,这是React添加effect前如何检查componentDidUpdate方法:

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

好的,咱们如今知道了render阶段中为ClickCounterFiber节点执行了什么操做。如今让咱们看看这些操做如何改变Fiber节点的值。当React开始work,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) => {…}
            }
        },
        ...
    }
}
复制代码

work完成后,咱们获得一个长这样的Fiber节点:

{
    effectTag: 4,
    elementType: class ClickCounter, firstEffect: null, memoizedState: {count: 1},
    type: class ClickCounter, stateNode: {
        state: {count: 1}
    },
    updateQueue: {
        baseState: {count: 1},
        firstUpdate: null,
        ...
    }
}
复制代码

花点时间观察属性值的差别

更新被应用后,memoizedStateupdateQueuebaseState的属性count的值变为1。React也更新了ClickCounter组件实例的state。

至此,队列中再也不有更新,因此firstUpdatenull。更重要的是,咱们改变了effectTag属性。它再也不是0,它的是为4。 二进制为100,意味着第三位被设置了,表明Update反作用标记

export const Update = 0b00000000100;
复制代码

能够得出结论,当执行ClickCounterFiber节点的work时,React低啊用变化前生命周期方法,更新state,定义有关的反作用。

协调ClickCounter Fiber的子组件

在那以后,React进入finishClassComponent。这是调用组件实例render方法和在子组件上使用diff算法的地方。文档中对此有高级概述。如下是相关部分:

当比较两个相同类型的React DOM元素时,React查看二者的属性(attributes),保留DOM节点,仅更新变化了的属性。

然而,若是咱们深刻挖掘,会知道它实际是对比Fiber节点和React元素。可是我如今不会详细介绍由于过程至关复杂。我会单独些篇文章,特别关注子协调过程。

若是你想本身学习细节,请查看reconcileChildrenArray函数,由于在咱们的程序中render方法返回一个React元素数组。

至此,有两个很重要的事须要理解。第一,当React进行子协调时,它会为从render函数返回的子React元素建立或更新Fiber节点。finishClassComponent函数当前Fiber节点的第一个子节点的引用。它被赋值给nextUnitOfWork并在稍后的work循环中处理。第二,React更新子节点的props做为父节点执行的一部分work。为此,它使用render函数返回的React元素的数据。

举例来讲,这是React协调ClickCounterfiber子节点以前span元素对应的Fiber节点看起来的样式

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

能够看到,memoizedPropspendingPropschildren属性都是0。这是render函数返回的span元素对应的React元素的结构。

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

能够看出,Finer节点和返回的React元素的props是有差别的createWorkInProgress内部用这建立替代的Fiber节点,React把React元素中更新的属性复制到Fiber节点

所以,在React完成ClickCounter组件子协调后,span的Fiber节点的pendingProps更新了。它们将匹配spanReact元素中的值。

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

稍后,React会为spanFiber节点执行work,它将把它们复制到memoizedProps以及添加effects来更新DOM。

好的,这就是render阶段React为ClickCounterfiber节点所执行的全部work。由于button是ClickCounter组件的第一个子节点,它会被赋值给nextUnitOfWork变量。button上无事可作,全部React会移动到它的兄弟节点spanFiber节点上。根据这里描述的算法,这发生在completeUnitOfWork函数内。

处理Span fiber的更新

nextUnitOfWork变量如今指向spanfiber的alternate,React基于它开始工做。和ClickCounter执行的步骤相似,开始于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节点的work

一旦beginWork完成,节点就进入completeWork函数。可是在那以前,React须要更新span Fiber节点的memoizedProps属性。你应该还记得协调ClickCounter组件子节点时更新了spanFiber节点的pendingProps

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

因此一旦spanfiber的beginWork完成,React会将pendingProps更新到memoizedProps

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

而后调用的completeWork和咱们看过的beginWork类似,基本上是一个大的switch语句。

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

因为spanFiber节点是HostComponent,它会执行updateHostComponent函数。在这个函数中React大致上作了这些事:

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

在这些操做执行前,spanFiber节点看起来像这样:

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

works完成后它看起来像这样:

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

注意effectTagupdateQueue字段的差别。它再也不是0,它的值是4。用二进制表示是100,意味着设置了第3位,正是Update反作用的标志位。这是React在接下来的commit阶段对这个节点惟一要作的任务。updateQueue保存着用于更新的载荷。

一旦React处理完ClickCounter级它的子节点,render阶段结束。如今它能够将完成的替代树赋值给FiberRootfinishedWork属性。这是须要被刷新到屏幕上的新树。它能够在render阶段以后立刻被处理,或这当React被浏览器给予时间时再处理。

Effects list

在咱们的例子中,因为span节点ClickCounter组件有反作用,React将添加指向spanFiber节点的连接到HostFiberfirstEffect属性。

React在compliteUnitOfWork函数内构建effects list。这是带有更新span节点文本和调用ClickCounter上hooks反作用的Fiber树看起来的样子:

这是由有反作用的节点组成的线性列表:

Commit阶段

这个阶段开始于completeRoot函数。它在作其余工做以前,它将FiberRootfinishedWork属性设为null

root.finishedWork = null;
复制代码

于以前的render阶段不一样的是,commit阶段老是同步的,这样它能够安全地更新HostRoot来表示commit work开始了。

commit阶段是React更新DOM和调用突变后生命周期方法componentDidUpdate的地方。为此,它遍历在render阶段中构建的effects list并应用它们。

有如下在render阶段为spanClickCounter定义的effects:

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

ClickCounter的effect tag的值是5或二进制的101,定义了对于类组件基本上转换为componentDidUpdate生命周期方法的Update工做。最低位也被设置了,表示该Fiber节点在render阶段的全部工做都已完成。

span的effect tag的值是4或二进制的100,定义了原生组件DOM更新的update工做。这个例子中的span元素,React须要更新这个元素的textContent

应用effects

让咱们看看React如何应用这些effects。commitRoot函数用于应用这些effects,由3个子函数组成:

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

每一个子函数都实现了一个循环,该循环用于遍历effects list并检查这些effects的类型。当发现effect和函数的目的有关时就应用它。咱们的例子中,它会调用ClickCounter组件的componentDidUpdate生命周期方法,更新span元素的文本。

第一个函数 commitBeforeMutationLifeCycles 寻找 Snapshot effect而后调用getSnapshotBeforeUpdate方法。可是,咱们在ClickCounter组件中没有实现该方法,React在render阶段没有添加这个effect。因此在咱们的例子中,这个函数不作任何事。

DOM更新

接下来React执行 commitAllHostEffects 函数。这儿是React将span元素的t文本由0变为1的地方。ClickCounter fiber没有要作的,由于类组件的节点没有任何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节点的updateQueue载荷更新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。它将替代树是设为当前树:

root.current = finishedWork;
复制代码

调用突变后生命周期hooks

剩下的函数是commitAllLifecycles。这是 React 调用突变后生命周期方法的地方。在render阶段,React为ClickCounter组件添加Update effect。这是commitAllLifecycles寻找的effects之一并调用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方法的函数。

相关文章
相关标签/搜索