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

3次阅读

共计 13635 个字符,预计需要花费 35 分钟才能阅读完成。

前言

在本文中咱们会看到 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. commi t 阶段之前,FiberRootfinishedWork 属性设置为null
  15. commit阶段是同步的,是 React 更新 DOM, 以及调用生命周期办法的中央(利用副作用)。

参考

  • In-depth explanation of state and props update in React
正文完
 0