「译」React Fiber 那些事: 深刻解析新的协调算法

React 是一个用于构建用户交互界面的 JavaScript 库,其核心 机制 就是跟踪组件的状态变化,并将更新的状态映射到到新的界面。在 React 中,咱们将此过程称之为协调。咱们调用 setState 方法来改变状态,而框架自己会去检查 state 或 props 是否已经更改来决定是否从新渲染组件。

React 的官方文档对 协调机制 进行了良好的抽象描述: React 的元素、生命周期、 render 方法,以及应用于组件子元素的 diffing 算法综合起到的做用,就是协调。从 render 方法返回的不可变的 React 元素一般称为「虚拟 DOM」。这个术语有助于早期向人们解释 React,但它也引发了混乱,而且再也不用于 React 文档。在本文中,我将坚持称它为 React 元素的树。react

除了 React 元素的树以外,框架老是在内部维护一个实例来持有状态(如组件、 DOM 节点等)。从版本 16 开始, React 推出了内部实例树的新的实现方法,以及被称之为 Fiber 的算法。若是想要了解 Fiber 架构带来的优点,能够看下 React 在 Fiber 中使用链表的方式和缘由。算法

这是本系列的第一篇文章,这一系列的目的就是向你描绘出 React 的内部架构。在本文中,我但愿可以提供一些与算法相关的重要概念和数据结构,并对其进行深刻阐述。一旦咱们有足够的背景,咱们将探索用于遍历和处理 Fiber 树的算法和主要功能。本系列的下一篇文章将演示 React 如何使用该算法执行初始渲染和处理 state 以及 props 的更新。到那时,咱们将继续讨论调度程序的详细信息,子协调过程以及构建 effect 列表的机制。数组

我将给你带来一些很是高阶的知识🧙。我鼓励你阅读来了解 Concurrent React 的内部工做的魔力。若是您计划开始为 React 贡献代码,本系列也将为您提供很好的指导。我是 逆向工程的坚决信徒,所以本文会有不少最新版本 16.6.0 中的源代码的连接。 须要消化的内容绝对是不少的,因此若是你当下还不能很理解的话,不用感到压力。花些时间是值得的。请注意,只是使用 React 的话,您不须要知道任何文中的内容。本文是关于 React 在内部是如何工做的。bash

我在 ag-Grid 担任开发人员倡导者。若是您想了解数据网格或寻找最终的 React 数据网格解决方案,请与咱们联系或尝试使用指南「在5分钟内开始使用 React 网格」。我很乐意回答您可能会有的任何问题。数据结构

背景介绍

以下是我将在整个系列中使用的一个简单的应用程序。咱们有一个按钮,点击它将会使屏幕上渲染的数字加 1:架构

而它的实现以下:并发

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};
        });
    }


    render() {
        return [
            <button key="1" onClick={this.handleClick}>Update counter</button>,
            <span key="2">{this.state.count}</span>
        ]
    }
}
复制代码

你能够在 这里 把玩一下。如您所见,它就是一个可从 render 方法返回两个子元素 -- button 和 span 的简单组件。只要你单击该按钮,组件的状态将在处理程序内被更新,而状态的更新就会致使 span 元素内的文本更新。框架

在协调阶段内,React 进行了各类各样的活动。例如,在咱们的简单应用程序中,从第一次渲染到状态更新后的期间内,React 执行了以下高阶操做:异步

  • 更新了 ClickCounter 组件的内部状态的 count 属性
  • 获取和比较了 ClickCounter 组件的子组件及其 props
  • 更新 span 元素的 props
  • 更新 span 元素的 textContent 属性

除了上述活动,React 在协调期间还执行了一些其余活动,如调用 生命周期方法 或更新 refs。全部这些活动在 Fiber 架构中统称为「工做」。工做类型一般取决于 React 元素的类型。例如,对于类定义的组件,React 须要建立实例,可是函数定义的组件就没必要执行此操做。正如咱们所了解的,React 中有许多元素类型,例如:类和函数组件,宿主组件(DOM 节点)portal 等。React 元素的类型由 createElement 函数的第一个参数定义,此函数一般在 render 方法中调用以建立元素。函数

在咱们开始探索活动细节和 Fiber 算法的主要内容以前,咱们首先来熟悉下 React 在内部使用的一些数据结构。

从 React 元素到 Fiber 节点

React 中的每一个组件都有一个 UI 表示,咱们能够称之为从 render 方法返回的一个视图或模板。这是 ClickCounter 组件的模板:

<button key="1" onClick={this.onClick}>Update counter</button>
<span key="2">{this.state.count}</span>
复制代码

React 元素

若是模板通过 JSX 编译器处理,你就会获得一堆 React 元素。这是从 React 组件的 render 方法返回的,但并非 HTML 。因为咱们并无被强制要求使用 JSX,所以咱们的 ClickCounter 组件的 render 方法能够像这样重写:

class ClickCounter {
    ...
    render() {
        return [
            React.createElement(
                'button',
                {
                    key: '1',
                    onClick: this.onClick
                },
                'Update counter'
            ),
            React.createElement(
                'span',
                {
                    key: '2'
                },
                this.state.count
            )
        ]
    }
}
复制代码

render 方法中调用的 React.createElement 会产生两个以下的数据结构:

[
    {
        $$typeof: Symbol(react.element),
        type: 'button',
        key: "1",
        props: {
            children: 'Update counter',
            onClick: () => { ... }
        }
    },
    {
        $$typeof: Symbol(react.element),
        type: 'span',
        key: "2",
        props: {
            children: 0
        }
    }
]
复制代码

能够看到,React 为这些对象添加了 $$typeof 属性,从而将它们惟一地标识为 React 元素。此外咱们还有属性 type、key 和 props 来描述元素。这些值取自你传递给 React.createElement 函数的参数。请注意React 如何将文本内容表示为 span 和 button 节点的子项,以及 click 钩子如何成为 button 元素 props 的一部分。 React 元素上还有其余字段,如 ref 字段,而这超出了本文的范围。

而 ClickCounter 的 React 元素就没有什么 props 或 key 属性:

{
    $$typeof: Symbol(react.element),
    key: null,
    props: {},
    ref: null,
    type: ClickCounter
}
复制代码

Fiber 节点

在协调期间,从 render 方法返回的每一个 React 元素的数据都会被合并到 Fiber 节点树中。每一个 React 元素都有一个相应的 Fiber 节点。与 React 元素不一样,不会在每次渲染时从新建立这些 Fiber 。这些是持有组件状态和 DOM 的可变数据结构。

咱们以前讨论过,根据不一样 React 元素的类型,框架须要执行不一样的活动。在咱们的示例应用程序中,对于类组件 ClickCounter ,它调用生命周期方法和 render 方法,而对于 span 宿主组件(DOM 节点),它进行得是 DOM 修改。所以,每一个 React 元素都会转换为 相应类型 的 Fiber 节点,用于描述须要完成的工做。

您能够将 Fiber 视为表示某些要作的工做的数据结构,或者说,是一个工做单位。Fiber 的架构还提供了一种跟踪、规划、暂停和销毁工做的便捷方式。

当 React 元素第一次转换为 Fiber 节点时,React 在 createFiberFromTypeAndProps 函数中使用元素中的数据来建立 Fiber。在随后的更新中,React 会再次利用 Fiber 节点,并使用来自相应 React 元素的数据更新必要的属性。若是再也不从 render 方法返回相应的 React 元素,React 可能还须要根据 key 属性来移动或删除层级结构中的节点。

查看 ChildReconciler 函数以查看 React 为现有 Fiber 节点执行的全部活动和相应函数的列表。

由于React为每一个 React 元素建立一个 Fiber 节点,而且由于咱们有一个这些元素组成的树,因此咱们能够获得一个 Fiber 节点树。对于咱们的示例应用程序,它看起来像这样:

全部 Fiber 节点都经过链表链接,具体是使用Fiber节点上的 child、sibling 和 return 属性。

current 树及 workInProgress 树

在第一次渲染以后,React 最终获得一个 Fiber 树,它反映了用于渲染 UI 的应用程序的状态。这棵树一般被称为 current 树(当前树)。当 React 开始处理更新时,它会构建一个所谓的 workInProgress 树(工做过程树),它反映了要刷新到屏幕的将来状态。

全部工做都在 workInProgress 树的 Fiber 节点上执行。当 React 遍历 current 树时,对于每一个现有 Fiber 节点,React 会建立一个构成 workInProgress 树的备用节点,这一节点会使用 render 方法返回的 React 元素中的数据来建立。处理完更新并完成全部相关工做后,React 将准备好一个备用树以刷新到屏幕。一旦这个 workInProgress 树在屏幕上呈现,它就会变成 current 树。

React 的核心原则之一是一致性。 React 老是一次性更新 DOM - 它不会显示部分中间结果。workInProgress 树充当用户不可见的「草稿」,这样 React 能够先处理全部组件,而后将其更改刷新到屏幕。

在源代码中,您将看到不少函数从 current 和 workInProgress 树中获取 Fiber 节点。这是一个这类函数的签名。

function updateHostComponent(current, workInProgress, renderExpirationTime) {...}

复制代码

每一个Fiber节点持有备用域在另外一个树的对应部分的引用。来自 current 树中的节点会指向 workInProgress 树中的节点,反之亦然。

反作用

咱们能够将 React 中的一个组件视为一个使用 state 和 props 来计算 UI 表示的函数。其余全部活动,如改变 DOM 或调用生命周期方法,都应该被视为反作用,或者简单地说是一种效果。文档中 是这样描述的:

您以前可能已经在 React 组件中执行数据提取,订阅或手动更改 DOM。咱们将这些操做称为“反作用”(或简称为“效果”),由于它们会影响其余组件,而且在渲染过程当中没法完成。

您能够看到大多 state 和 props 更新都会致使反作用。既然使用反作用是工做(活动)的一种类型,Fiber 节点是一种方便的机制来跟踪除了更新之外的效果。每一个 Fiber 节点均可以具备与之相关的反作用,它们可在 effectTag 字段中编码。

所以,Fiber 中的反作用基本上定义了处理更新后须要为实例完成的 工做。对于宿主组件(DOM 元素),所谓的工做包括添加,更新或删除元素。对于类组件,React可能须要更新 refs 并调用 componentDidMount 和 componentDidUpdate 生命周期方法。对于其余类型的 Fiber ,还有相对应的其余反作用。

反作用列表

React 处理更新的素对很是迅速,为了达到这种水平的性能,它采用了一些有趣的技术。其中之一是构建具备反作用的 Fiber 节点的线性列表,从而可以快速遍历。遍历线性列表比树快得多,而且没有必要在没有反作用的节点上花费时间。

此列表的目标是标记具备 DOM 更新或其余相关反作用的节点。此列表是 finishedWork 树的子集,并使用 nextEffect 属性而不是 current 和 workInProgress 树中使用的 child 属性进行连接。

Dan Abramov 为反作用列表提供了一个类比。他喜欢将它想象成一棵圣诞树,「圣诞灯」将全部有效节点捆绑在一块儿。为了使这个可视化,让咱们想象以下的 Fiber 节点树,其中标亮的节点有一些要作的工做。例如,咱们的更新致使 c2 被插入到 DOM 中,d2 和 c1 被用于更改属性,而 b2 被用于触发生命周期方法。反作用列表会将它们连接在一块儿,以便 React 稍后能够跳过其余节点:

能够看到具备反作用的节点是如何连接在一块儿的。当遍历节点时,React 使用 firstEffect 指针来肯定列表的开始位置。因此上面的图表能够表示为这样的线性列表:

如您所见,React 按照从子到父的顺序应用反作用。

Fiber 树的根节点

每一个 React 应用程序都有一个或多个充当容器的 DOM 元素。在咱们的例子中,它是带有 ID 为 container 的 div 元素。React 为每一个容器建立一个 Fiber 根 对象。您可使用对 DOM 元素的引用来访问它:

const fiberRoot = query('#container')._reactRootContainer._internalRoot
复制代码

这个 Fiber 根是React保存对 Fiber 树的引用的地方,它存储在 Fiber 根对象的 current 属性中:

const hostRootFiberNode = fiberRoot.current
复制代码

Fiber 树以 一个特殊类型 的 Fiber 节点 HostRoot 开始。它在内部建立的,并充当最顶层组件的父级。HostRoot 节点可经过 stateNode 属性返回到 FiberRoot:

fiberRoot.current.stateNode === fiberRoot; // true
复制代码

你能够经过 Fiber 根访问最顶层的 HostRoot 节点来探索 Fiber 树,或者能够从组件实例中获取单独的 Fiber 节点,以下所示:

compInstance._reactInternalFiber
复制代码

Fiber 节点结构

如今让咱们看一下为 ClickCounter 组件建立的 Fiber 节点的结构

{
    stateNode: new ClickCounter,
    type: ClickCounter,
    alternate: null,
    key: null,
    updateQueue: null,
    memoizedState: {count: 0},
    pendingProps: {},
    memoizedProps: {},
    tag: 1,
    effectTag: 0,
    nextEffect: null
}
复制代码

以及 span DOM 元素

{
    stateNode: new ClickCounter,
    type: ClickCounter,
    alternate: null,
    key: null,
    updateQueue: null,
    memoizedState: {count: 0},
    pendingProps: {},
    memoizedProps: {},
    tag: 1,
    effectTag: 0,
    nextEffect: null
}
复制代码

Fiber 节点上有不少字段。我在前面的部分中描述了字段 alternate、 effectTag 和 nextEffect 的用途。如今让咱们看看为何须要其余字段。

stateNode

保存组件的类实例、DOM 节点或与 Fiber 节点关联的其余 React 元素类型的引用。总的来讲,咱们能够认为该属性用于保持与一个 Fiber 节点相关联的局部状态。

type

定义此 Fiber 节点的函数或类。对于类组件,它指向构造函数,对于 DOM 元素,它指定 HTML 标记。我常用这个字段来理解 Fiber 节点与哪一个元素相关。

tag

定义 Fiber 的类型。它在协调算法中用于肯定须要完成的工做。如前所述,工做取决于React元素的类型。函数 createFiberFromTypeAndProps 将 React 元素映射到相应的 Fiber 节点类型。在咱们的应用程序中,ClickCounter 组件的属性 tag 是 1,表示是 ClassComponent(类组件),而 span 元素的属性 tag 是 5,表示是 HostComponent(宿主组件)。

updateQueue

状态更新、回调和 DOM 更新的队列。

memoizedState

用于建立输出的 Fiber 状态。处理更新时,它会反映当前在屏幕上呈现的状态。

memoizedProps

在前一个渲染中用于建立输出的 Fiber 的 props。

pendingProps

已从 React 元素中的新数据更新而且须要应用于子组件或 DOM 元素的 props。

key

惟一标识符,当具备一组子元素的时候,可帮助 React 肯定哪些项发生了更改、添加或删除。它与 此处 描述的 React 的「列表和键」功能有关。

您能够在 此处 找到 Fiber 节点的完整结构。我在上面的解释中省略了一堆字段。特别是,我跳过了指针 child、sibling 和 return,它们构成了我在 上一篇文章 中描述的树数据结构。以及专属于 Scheduler 的 expirationTime,childExpirationTime 和 mode 等字段类别。

通用算法

React 在两个主要阶段执行工做:render 和 commit。

在第一个 render 阶段,React 经过 setUpdate 或 React.render 计划性的更新组件,并肯定须要在 UI 中更新的内容。若是是初始渲染,React 会为 render 方法返回的每一个元素建立一个新的 Fiber 节点。在后续更新中,现有 React 元素的 Fiber 节点将被重复使用和更新。这一阶段是为了获得标记了反作用的 Fiber 节点树。反作用描述了在下一个commit阶段须要完成的工做。在当前阶段,React持有标记了反作用的 Fiber 树并将其应用于实例。它遍历反作用列表、执行 DOM 更新和用户可见的其余更改。

咱们须要重点理解的是,第一个 render 阶段的工做是能够异步执行的。React 能够根据可用时间片来处理一个或多个 Fiber 节点,而后停下来暂存已完成的工做,并转而去处理某些事件,接着它再从它中止的地方继续执行。但有时候,它可能须要丢弃完成的工做并再次从顶部开始。因为在此阶段执行的工做不会致使任何用户可见的更改(如 DOM 更新),所以暂停行为才有了意义。与之相反的是,后续 commit 阶段始终是同步的。这是由于在此阶段执行的工做会致使用户可见的变化,例如 DOM 更新。这就是为何 React 须要在一次单一过程当中完成这些更新。

React 要作的一种工做就是调用生命周期方法。一些方法是在 render 阶段调用的,而另外一些方法则是在 commit 阶段调用。这是在第一个 render 阶段调用的生命周期列表:

  • [UNSAFE_]componentWillMount(弃用)
  • [UNSAFE_]componentWillReceiveProps(弃用)
  • getDerivedStateFromProps
  • shouldComponentUpdate
  • [UNSAFE_]componentWillUpdate(弃用)
  • render

正如你所看到的,从版本 16.3 开始,在 render 阶段执行的一些保留的生命周期方法被标记为 UNSAFE,它们如今在文档中被称为遗留生命周期。它们将在将来的 16.x 发布版本中弃用,而没有 UNSAFE 前缀的方法将在 17.0 中移除。您能够在 此处 详细了解这些更改以及建议的迁移路径。

那么这么作的目的是什么呢?

好吧,咱们刚刚了解到,由于 render 阶段不会产生像 DOM 更新这样的反作用,因此 React 能够异步处理组件的异步更新(甚至可能在多个线程中执行)。可是,标有 UNSAFE 的生命周期常常被误解和滥用。开发人员倾向于将带有反作用的代码放在这些方法中,这可能会致使新的异步渲染方法出现问题。虽然只有没有 UNSAFE 前缀的对应方法将被删除,但它们仍可能在即将出现的并发模式(您能够选择退出)中引发问题。

接下来罗列的生命周期方法是在第二个 commit 阶段执行的:

  • getSnapshotBeforeUpdate
  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount

由于这些方法都在同步的 commit 阶段执行,他们可能会包含反作用,并对 DOM 进行一些操做。

至此,咱们已经有了充分的背景知识,下面咱们能够看下用来遍历树和执行一些工做的通用算法。

Render 阶段

协调算法始终使用 renderRoot 函数从最顶层的 HostRoot 节点开始。不过,React 会略过已经处理过的 Fiber 节点,直到找到未完成工做的节点。例如,若是在组件树中的深层组件中调用 setState 方法,则 React 将从顶部开始,但会快速跳过各个父项,直到它到达调用了 setState 方法的组件。

工做循环的主要步骤

全部的 Fiber 节点都会在 工做循环 中进行处理。以下是该循环的同步部分的实现:

function workLoop(isYieldy) {
  if (!isYieldy) {
    while (nextUnitOfWork !== null) {
      nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    }
  } else {...}
}
复制代码

在上面的代码中,nextUnitOfWork 持有 workInProgress 树中的 Fiber 节点的引用,这个树有一些工做要作。当 React 遍历 Fiber 树时,它会使用这个变量来知晓是否有任何其余 Fiber 节点具备未完成的工做。处理过当前 Fiber 后,变量将持有树中下一个 Fiber 节点的引用或 null。在这种状况下,React 退出工做循环并准备好提交更改。 遍历树、初始化或完成工做主要用到 4 个函数:

  • performUnitOfWork
  • beginWork
  • completeUnitOfWork
  • completeWork

为了演示他们的使用方法,咱们能够看看以下展现的遍历 Fiber 树的动画。我已经在演示中使用了这些函数的简化实现。每一个函数都须要对一个 Fiber 节点进行处理,当 React 从树上下来时,您能够看到当前活动的 Fiber 节点发生了变化。从视频中咱们能够清楚地看到算法如何从一个分支转到另外一个分支。它首先完成子节点的工做,而后才转移到父节点进行处理。

注意,垂直方向的连线表示同层关系,而折线链接表示父子关系,例如,b1 没有子节点,而 b2 有一个子节点 c1。

在这个 视频 中咱们能够暂停播放并检查当前节点和函数的状态。从概念上讲,你能够将「开始」视为「进入」一个组件,并将「完成」视为「离开」它。在解释这些函数的做用时,您也能够在 这里 使用示例和实现。

咱们首先开始研究 performUnitOfWork 和 beginWork 这两个函数:

function performUnitOfWork(workInProgress) {
    let next = beginWork(workInProgress);
    if (next === null) {
        next = completeUnitOfWork(workInProgress);
    }
    return next;
}

function beginWork(workInProgress) {
    console.log('work performed for ' + workInProgress.name);
    return workInProgress.child;
}
复制代码

函数 performUnitOfWork 从 workInProgress 树接收一个 Fiber 节点,并经过调用 beginWork 函数启动工做。这个函数将启动全部 Fiber 执行工做所须要的活动。出于演示的目的,咱们只 log 出 Fiber 节点的名称来表示工做已经完成。函数 beginWork 始终返回指向要在循环中处理的下一个子节点的指针或 null。

若是有下一个子节点,它将被赋值给 workLoop 函数中的变量 nextUnitOfWork。可是,若是没有子节点,React 知道它到达了分支的末尾,所以它能够完成当前节点。一旦节点完成,它将须要为同层的其余节点执行工做,并在完成后回溯到父节点。这是 completeUnitOfWork 函数执行的代码:

function completeUnitOfWork(workInProgress) {
    while (true) {
        let returnFiber = workInProgress.return;
        let siblingFiber = workInProgress.sibling;

        nextUnitOfWork = completeWork(workInProgress);

        if (siblingFiber !== null) {
            // If there is a sibling, return it
            // to perform work for this sibling
            return siblingFiber;
        } else if (returnFiber !== null) {
            // If there's no more work in this returnFiber, // continue the loop to complete the parent. workInProgress = returnFiber; continue; } else { // We've reached the root.
            return null;
        }
    }
}

function completeWork(workInProgress) {
    console.log('work completed for ' + workInProgress.name);
    return null;
复制代码

你能够看到函数的核心就是一个大的 while 的循环。当 workInProgress 节点没有子节点时,React 会进入此函数。完成当前 Fiber 节点的工做后,它就会检查是否有同层节点。若是找的到,React 退出该函数并返回指向该同层节点的指针。它将被赋值给 nextUnitOfWork 变量,React将从这个节点开始执行分支的工做。咱们须要着重理解的是,在当前节点上,React 只完成了前面的同层节点的工做。它还没有完成父节点的工做。只有在完成以子节点开始的全部分支后,才能完成父节点和回溯的工做。

从实现中能够看出,performUnitOfWork 和 completeUnitOfWork 主要用于迭代目的,而主要活动则在 beginWork 和 completeWork 函数中进行。在后续的系列文章中,咱们将了解随着 React 进入 beginWork 和 completeWork 函数,ClickCounter 组件和 span 节点会发生什么。

commit 阶段

这一阶段从函数 completeRoot 开始。在这个阶段,React 更新 DOM 并调用变动生命周期以前及以后方法的地方。

当 React 进入这个阶段时,它有 2 棵树和反作用列表。第一个树表示当前在屏幕上渲染的状态,而后在 render 阶段会构建一个备用树。它在源代码中称为 finishedWork 或 workInProgress,表示须要映射到屏幕上的状态。此备用树会用相似的方法经过 child 和 sibling 指针连接到 current 树。

而后,有一个反作用列表 -- 它是 finishedWork 树的节点子集,经过 nextEffect 指针进行连接。须要记住的是,反作用列表是运行 render 阶段的结果。渲染的重点就是肯定须要插入、更新或删除的节点,以及哪些组件须要调用其生命周期方法。这就是反作用列表告诉咱们的内容,它页正是在 commit 阶段迭代的节点集合。

出于调试目的,能够经过 Fiber 根的属性 current访问 current 树。能够经过 current 树中 HostFiber 节点的 alternate 属性访问 finishedWork 树。

  • commit 阶段运行的主要函数是 commitRoot 。它执行以下下操做:
  • 在标记为 Snapshot 反作用的节点上调用 getSnapshotBeforeUpdate 生命周期
  • 在标记为 Deletion 反作用的节点上调用 componentWillUnmount 生命周期
  • 执行全部 DOM 插入、更新、删除操做
  • 将 finishedWork 树设置为 current
  • 在标记为 Placement 反作用的节点上调用 componentDidMount 生命周期
  • 在标记为 Update 反作用的节点上调用 componentDidUpdate 生命周期

在调用变动前方法 getSnapshotBeforeUpdate 以后,React 会在树中提交全部反作用,这会经过两波操做来完成。第一波执行全部 DOM(宿主)插入、更新、删除和 ref 卸载。而后 React 将 finishedWork 树赋值给 FiberRoot,将 workInProgress 树标记为 current 树。这是在提交阶段的第一波以后、第二波以前完成的,所以在 componentWillUnmount 中前一个树仍然是 current,在 componentDidMount/Update 期间已完成工做是 current。在第二波,React 调用全部其余生命周期方法和引用回调。这些方法单独传递执行,从而保证整个树中的全部放置、更新和删除可以被触发执行。

如下是运行上述步骤的函数的要点:

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

这些子函数中都实现了一个循环,该循环遍历反作用列表并检查反作用的类型。当它找到与函数目的相关的反作用时,就会执行。

更新前的生命周期方法

例如,这是在反作用树上遍历并检查节点是否具备 Snapshot 反作用的代码:

function commitBeforeMutationLifecycles() {
    while (nextEffect !== null) {
        const effectTag = nextEffect.effectTag;
        if (effectTag & Snapshot) {
            const current = nextEffect.alternate;
            commitBeforeMutationLifeCycles(current, nextEffect);
        }
        nextEffect = nextEffect.nextEffect;
    }
}
复制代码

对于一个类组件,这一反作用意味着会调用 getSnapshotBeforeUpdate 生命周期方法。

DOM 更新

commitAllHostEffects 是 React 执行 DOM 更新的函数。该函数基本上定义了节点须要完成的操做类型,并执行这些操做:

function commitAllHostEffects() {
    switch (primaryEffectTag) {
        case Placement: {
            commitPlacement(nextEffect);
            ...
        }
        case PlacementAndUpdate: {
            commitPlacement(nextEffect);
            commitWork(current, nextEffect);
            ...
        }
        case Update: {
            commitWork(current, nextEffect);
            ...
        }
        case Deletion: {
            commitDeletion(nextEffect);
            ...
        }
    }
}
复制代码

更新后的生命周期方法

commitAllLifecycles 是 React 调用全部剩余生命周期方法的函数。在 React 的当前实现中,惟一会调用的变动方法就是 componentDidUpdate。

咱们终于讲完了。让我知道您对该文章的见解或在评论中提出问题。我还有关于调度程序、子调和过程以及如何构建反作用列表的文章来对这些内容提供深刻的解释。我还计划制做一个视频,在这里我将展现如何使用本文做为基础来调试应用程序。

转载地址: www.yuque.com/es2049/blog…
相关文章
相关标签/搜索