关于javascript:深入了解React中state和props的更新

前言

在本文中咱们会看到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

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理