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

栏目: 服务器 · 发布时间: 5年前

内容简介:原文链接:到react fiber内部一探究竟在我之前文章

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

到react fiber内部一探究竟

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

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

我已经概述过将在这篇文章中用到的主要数据结构和概念,特别是Fiber节点、当前和工作过程树、副作用和作用列表,我也对主要的算法提供过大致的说明,且解释过** render commit **阶段的不同。如果你还没有读过,那我建议你从那里还是

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

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

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

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如何处理状态更新以及构建作用列表,我们将带你去看看** render commit **阶段中大致方法都做了些什么。

特别的是,我们将看到React在 completeWork 中是如何:

  • 更新** ClickCounter state 中的 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 **阶段被执行

  • 在组件实例上更新** state props **

state props 应该在组件实例的 render 方法调用之前被更新,因为 render 方法的输出通常依赖于 state props ,如果我们不这样做,那它将总是返回同样的结果。

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

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 属性的值在 memoizedState updateQueue 中的 baseState 上变成 1 ,React也更新了** ClickCounter **组件实例中的state。

此刻,我们在队列中不在有更新,所有** firstUpdate null ,且重要的是,我们修改了 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},
    ...
}
复制代码

正如你所见, memoizedProps pendingProps children 属性值都是 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 组件上的子协调后, span Fiber节点的 pendingProps 将会更新,它们将会匹配 span **React元素中的值:

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

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

嗯,这就是在render阶段中,React在** ClickCounter Fiber节点上执行的所有工作,因为Button是 ClickCounter 组件上的第一孩子,它将赋值给 nextUnitOfWork 变量,由于无事可做,所以React将会转移到它的兄弟 span Fiber节点上,根据这里描述的算法,这发生在 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更新 span Fiber节点上的 pendingProps **:

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

所以,一旦** span Fiber上的 beginWork 完成,React就是更新 pendingProps memoizedProps **上:

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

因为我们的** span Fiber节点是 HostComponent **,所以它执行 updateHostComponent 方法,在这个方法中,React基本上如下操作:

  • 准备DOM更新
  • 把它们添加到** span fiber的 updateQueue **
  • 添加更新DOM的作用

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

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

当工作完成之后,它看起来像:

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

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

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

作用列表(Effects list)

在我们的例子中,因为 span 节点和 ClickCounter 组件都有副作用,React会把 HostFiber 上的 firstEffect 指向 span Fiber节点。

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

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

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

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

Commit 阶段

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

root.finishedWork = null;

不像** render 阶段, commit 阶段总是同步的,所以它可以安全的更新 HostRoot **来指示提交工作开始了。

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

我们在** render 阶段中,对 span ClickCounter **节点有如下作用:

{ 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 **方法。

额……讲完啦。


以上所述就是小编给大家介绍的《[译] 深入解析React中state和props更新》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Web程序设计

Web程序设计

塞巴斯塔 / 2008-6 / 68.00元

《Web程序设计(第4版)》是最新版,介绍了Internet和万维网的起源及演变过程,全面系统地讨论了Web开发相关的主要编程语言和工具,以及这些语言和工具之间的相互影响及优劣势。该书对全书内容进行了很多修订,并新增加了关于Ruby、Rails和Ajax的3个章节。一起来看看 《Web程序设计》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具