[转]-学习React-Fiber

前言

React16 提出了 Fiber 结构,其能够将任务分片,划分优先级,同时能够实现类似于操作系统中对线程的抢占式调度,非常强大。

正文

React 是一个用于构建 UI 的 JavaScript 库,其核心是跟踪组件状态变化并将更新到 view 上。在 React 中,我们将此过程视为 reconciliation。在调用 setState 方法后,框架会检查 state 或 props 是否已更改并在 UI 上重新呈现组件。

React 的文档提供了一种更高层次的对这种机制的描述:包含 React 元素的作用,生命周期方法和渲染方法,以及应用于组件子元素的 diffing 算法等相关内容。从 render 方法返回的不可变 React 元素树通常称为 “Virtual DOM”。这个术语有助于早期向人们解释 React,但它也引起了歧义,并且不再在 React 文档中使用。在本文中,将坚持称它为 React 元素的树。

除了 React 元素的树之外,框架总是有一个用于保持状态的内部实例树 (internal instances)(组件,DOM 节点等),与之相对的是跟具体平台有关的 public instance,也被称为 Host instance 。从 React 16 开始,React 推出了该内部实例树的新实现以及负责操作树的算法,被称为 Fiber。接下来我们将了解 fiber 架构带来的优势,了解 React 在 fiber 中使用链表的方式和原因

这是本系列的第一篇文章,旨在教你 React 的内部架构。在本文中,我想提供其中的重要概念和以及与算法相关的数据结构。一旦我们有足够的背景,我们将探索用于遍历和处理 fiber tree 的算法和主要功能。本系列的下一篇文章将演示 React 如何使用该算法执行初始渲染和处理 state 以及 props 更新。从那里我们将继续讨论 scheduler 的详细信息,子协调过程以及构建 effect list 的机制。

React 的核心思想

React框架在内存中维护了一个虚拟DOM树,根据数据的变化自动更新虚拟DOM,得到一个新虚拟DOM,然后通过diff算法对新老的虚拟DOM树进对比,通过对比得出变化的部分,得到一个Change(Patch),然后将这个Patch加入列队。最终批量更新Patch到DOM中。

React 16之前的不足

React的工作过程。通过render()setState()进行组的渲染和更新时,React主要有两个阶段:Reconclier,Renderer

Reconclier(调和阶段)

React会自动向下递归,遍历新数据生成新的Virtural DOM,然后通过diff算法,找到需要变更的元素(Patch),放到更新列队里面去。

Renderer(渲染阶段)

遍历更新队列,通过调用宿主API(DOM,Native,WebGL),更新渲染对应的元素。

协调阶段,采用的是递归的遍历方式,这种方式也被成为Stack Reconciler,主要是为了区分Fiber Reconciler取得一个名字。

Stack Reconciler 一旦任务开始进行,就无法中断,那么js将一直占用主线程,一直要等到整个虚拟DOM树计算完成才能把执行权交给渲染引擎,这样会导致用户的交互、动画等任务无法立即处理。

为了解决这个问题,把渲染更新过程中查分为多个子任务,每次只做小部分更新,等做完看是否还有剩余的时间,如果有继续下一个任务;如果没有,挂起当前任务,将时间控制权交给主线程,等主线程不忙的时候在继续执行。这种策略叫做Cooperative Scheduling(合作式调度), 操作系统常用任务调度策略之一。

合作式调度任务主要是来分配任务的。当有更新任务的时候,不会立即做Diff操作,而是先把当前的更新送入一个Update Queue中,然后交给Scheduler去处理,Scheduler会根据当前主线程的使用情况去处理这次的Update。这种特性的实现,使用了requestIdelCallbackApi。对于不支持的浏览器,React会加上pollyfill。

浏览器是一帧一帧执行的,在两个帧之间,主线程通常会有一小段空闲时间,requestIdelCallback可以在这个空闲期调用空闲期回调,执行一些任务。

  • 低优先级任务由requestIdelCallback处理;
  • 高优先级的任务,如动画相关的由requestAnimationFrame处理;
  • requestIdelCallback可以在多个空闲期调用空闲期回调,执行任务;
  • requestIdelCallback方法提供 deadline,即任务执行限制时间,用来切分任务,避免长时间执行,阻塞UI渲染而导致的掉帧;

实现可能遇到的问题:

  • 如何查分成子任务?
  • 一个子任务多大合适?
  • 怎么判断是否还有剩余时间?
  • 有剩余时间怎么去调度应该执行哪一个任务?
  • 没有剩余时间之前的任务该怎么办?

Fiber 架构就是用来解决这个问题的

Fiber 是什么?

Fiber是重新实现的堆栈帧,本质上Fiber可以理解为一个虚拟的堆栈帧,将可中断的任务拆分成多个子任务,通过按照优先级来自由调度子任务,分段更新,从而将之前的同步渲染任务改成异步渲染任务。所以理解Fiber是一种数据结构(堆栈帧),也可以理解成一种解决可中断的调用任务的一种解决方案,它的特性就是时间分片(time slicing)和暂停(supense)。 为了做到这些,需要将一种方法将任务分解为单元,Fiber代表一种工作单元。

Fiber是怎么工作的

  • React.render()setState()的时候开始创建更新。
  • 将创建的更新加入任务列队,等待调度。
  • requestIdelCallback空闲时执行任务。
  • 从根节点开始遍历 Fiber Node,并且构建WokeInProgress Tree。
  • 生成 EffectList
  • 根据 EffectList更新DOM。

首先写一个非常简单的程序:

import React, { Component } from 'react';

export default class ClickCounter extends 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>{this.state.count}</span>
        ]
    }
}


import React, { Component } from 'react';

export default class ClickCounter extends 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>{this.state.count}</span>
        ]
    }
}


可以查看在线实例。可以看到,它是一个非常简单的组件,它从 render 方法返回两个子元素 button 和 span。单击 button 后,组件的 state 将在处理程序内更新,这会导致 span 元素的文本更新。
React 会在 reconciliation 期间执行各种活动。例如,以下是 React 在上面这个程序中第一次渲染和状态更新之后执行的高级操作:

  1. 更新 state 中的 count 属性
  2. 检索并比较 ClickCounter 子组件以及 props
  3. 更新 span 元素的 props

同时,在 reconciliation 期间,还会执行其他活动包括调用生命周期方法更新引用。所有这些活动在 fiber 架构中统称为 “work”。work 类型通常取决于 React 元素的类型。例如,对于 class 组件,React 需要创建实例,而不是为 function 组件执行此操作。并且,React 中有许多元素,例如:class 和 function 组件,Host 组件(DOM 节点),protals 等. React 元素的类型由 createElement 函数的第一个参数定义,此函数通常在 render 方法中用于创建元素。总结起来,就是更新组件的内部状态,触发 side-effects 执行。
在我们开始探索 fiber 算法之前,让我们首先熟悉 React 内部使用的数据结构。

从 React Elements 到 Fiber nodes

React 中的每个组件都有一个 UI 表示,这个 UI 可以通过调用一个 view 或一个从 render 方法返回。这是 ClickCounter 组件的模板:

<button key="1" onClick={this.onClick}>Update counter</button>
<span>{this.state.count}</span>


<button key="1" onClick={this.onClick}>Update counter</button>
<span>{this.state.count}</span>


React Elements

一旦模板通过 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
            )
        ]
    }
}


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
        }
    }
]


[
    {
        $$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 处理程序如何成为按钮元素 props 的一部分。 React 元素上还有其他字段,如 ref 字段,超出了本文的范围,不再阐述。

同时 ClickCouter 元素没有任何的 props 或者 key:

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


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


Fiber nodes

在 reconciliation 期间,来自 render 方法返回的每个 React 元素的数据被合并到 fiber node 树中,每个 React 元素都有一个相应的 fiber node。与 React 元素不同,每次渲染过程,不会再重新创建 fiber。这些可变的数据包含组件 state 和 DOM。 我们之前讨论过,根据 React 元素的类型,框架需要执行不同的活动。在我们的示例应用程序中,对于 class 组件 ClickCounter,它调用生命周期方法和 render 方法,而对于 span Host 组件(DOM 节点),它执行 DOM 更新。因此,每个 React 元素都会转换为相应类型的 Fiber 节点,用于描述需要完成的工作。

可以这样认为:fiber 作为一种数据结构,用于代表某些 worker,换句话说,就是一个 work 单元,通过 Fiber 的架构,提供了一种跟踪,调度,暂停和中止工作的便捷方式。

当 React 元素第一次转换为 fiber 节点时,React 使用 createElement 返回的数据来创建 fiber,这段代码在 createFiberFromTypeAndProps 函数中。在随后的更新中,React 重用 fiber 节点,并使用来自相应 React 元素的数据来更新必要的属性。如果不再从 render 方法返回相应的 React 元素,React 可能还需要根据 key 来移动层次结构中的节点或删除它。

可以查看 ChildReconciler 函数的实现,来了解 React 为现有 fiber 节点执行的所有活动和相应函数的列表。因为 React 为每个 React 元素创建了一个 fiber node,并且因为我们有一个这些元素的树,所以我们将拥有一个 fiber node tree。对于我们的示例应用程序,它看起来像这样:

所有 fiber 节点都通过使用 fiber 节点上的以下属性:child,sibling 和 return 来构成一个 fiber node 的 linked list(后面我们称之为链表)。有关它为什么以这种方式工作的更多详细信息,可以查看这篇文章

Current and work in progress trees

在第一次渲染之后,React 最终得到一个 fiber tree,它反映了用于渲染 UI 的应用程序的状态。这棵树通常被称为 current tree。当 React 开始处理更新时,它会构建一个所谓的 workInProgress tree,它反映了要刷新到屏幕的未来状态。
所有 work 都在 workInProgress tree 中的 fiber 上执行。当 React 遍历 current tree 时,对于每个现有 fiber 节点,它会使用 render 方法返回的 React 元素中的数据创建一个备用 (alternate)fiber 节点,这些节点用于构成 workInProgress tree(备用 tree)。处理完更新并完成所有相关工作后,React 将备用 tree 刷新到屏幕。一旦这个 workInProgress tree 在屏幕上呈现,它就会变成 current tree。
React 的核心原则之一是一致性。 React 总是一次更新 DOM - 它不会显示部分结果。 workInProgress tree 对用户不可见,因此 React 可以先处理完所有组件,然后将其更改刷新到屏幕。

在源代码中,可以看到很多函数从 current tree 和 workInProgress tree 中获取 fiber 节点:

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

每个 fiber 节点都会通过 alternate 字段保持对另一个树的对应节点的引用。current tree 中的节点指向 workInProgress tree 中的备用节点,反之亦然。

Side-effects

我们可以将 React 中的一个组件视为一个使用 state 和 props 来计算 UI 的函数。每个其他活动,如改变 DOM 或调用生命周期方法,都应该被认为是 side-effects,react 文档中是这样描述的 side-effects 的:

You’ve likely performed data fetching, subscriptions, or manually changing the DOM 的_from React components before. We call these operations “side effects” (or “effects” for short) because they can affect other components and can’t be done during rendering._

可以看到大多数 state 和 props 更新将 side-effects。由于应用 effects 是一种 work,fiber 节点是一种方便的机制,可以跟踪除更新之外的 effects。每个 fiber 节点都可以具有与之相关的 effects, 通过 fiber 节点中的 effectTag 字段表示。
因此,Fiber 中的 effects 基本上定义了处理更新后需要为实例完成的工作,对于 Host 组件(DOM 元素),工作包括添加,更新或删除元素。对于 class 组件,React 可能需要更新 ref 并调用 componentDidMount 和 componentDidUpdate 生命周期方法,还存在与其他类型的 fiber 相对应的其他 effects。

Effects list

React 能够非常快速地更新,并且为了实现高性能,它采用了一些有趣的技术。其中之一是构建带有 side-effects 的 fiber 节点的线性列表,其具有快速迭代的效果。迭代线性列表比树快得多,并且没有必要在没有 side effects 的节点上花费时间。
此列表的目标是标记具有 DOM 更新或与其关联的其他 effects 的节点,此列表是 finishedWork tree 的子集,并使用 nextEffect 属性,而不是 current 和 workInProgress 树中使用的 child 属性进行链接。
Dan Abramove 为 effecs list 提供了一个类比: 他喜欢将它想象成一棵圣诞树,“圣诞灯” 将所有带有 effects 的节点绑定在一起。为了使这个 effects list 可视化,让我们想象下面的 fiber node tree,其中橙色的节点都有一些 effects 需要处理。例如,我们的更新导致 c2 被插入到 DOM 中,d2 和 c1 被用于更改属性,而 b2 被用于激活生命周期方法。effects list 将它们链接在一起,以便 React 可以在以后跳过其他节点:

你可以看到带有 effects 的节点是如何链接在一起的,当遍历节点时,React 使用 firstEffect 指针来确定 effects list 的开始位置。所以上图可以表示为这样的线性列表

Root of the fiber tree

每个 React 应用程序都有一个或多个作为 container 的 DOM 元素。在我们的例子中,它是带有 id 为 “container” 的 div 元素。

const domContainer = document.querySelector('#container');
ReactDOM.render(React.createElement(ClickCounter), domContainer);


const domContainer = document.querySelector('#container');
ReactDOM.render(React.createElement(ClickCounter), domContainer);


React 为每个 container 创建一个 fiber root 对象,可以使用对 DOM 元素的引用来访问它:

const fiberRoot = query('#container')._reactRootContainer._internalRoot


const fiberRoot = query('#container')._reactRootContainer._internalRoot


这个 fiber root 是 React 保存对 fiber tree 引用的地方。它存储在 fiber tree 的 current 属性中:

const hostRootFiberNode = fiberRoot.current


const hostRootFiberNode = fiberRoot.current


fiber tree 以特殊类型的 fiber 节点(HostRoot)开始。它是在内部创建的,并充当最顶层组件的父级,HostRoot fiber 节点通过 stateNode 属性指向 FiberRoot:

fiberRoot.current.stateNode === fiberRoot; // true
fiberRoot.current.stateNode === fiberRoot; // true

可以通过 fiber root 访问最顶端的 HostRoot 的 fiber node 来探索 fiber tree。或者,可以从组件实例中获取单个 fiber 节点,如下所示:

compInstance._reactInternalFiber
compInstance._reactInternalFiber

Fiber node structure

现在让我们看一下为 ClickCounter 组件创建的 fiber 节点的结构:

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


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


以及 span 节点:

{
    stateNode: new HTMLSpanElement,
    type: "span",
    alternate: null,
    key: "2",
    updateQueue: null,
    memoizedState: null,
    pendingProps: {children: 0},
    memoizedProps: {children: 0},
    tag: 5,
    effectTag: 0,
    nextEffect: null
}


{
    stateNode: new HTMLSpanElement,
    type: "span",
    alternate: null,
    key: "2",
    updateQueue: null,
    memoizedState: null,
    pendingProps: {children: 0},
    memoizedProps: {children: 0},
    tag: 5,
    effectTag: 0,
    nextEffect: null
}


fiber 节点上有很多字段,我在前面的部分中描述了 alternate 字段,effectTag 和 nextEffect 的用途。现在让我们看看为什么我们需要其他字段:

  • stateNode:保存对组件的类实例,DOM 节点或与 fiber 节点关联的其他 React 元素类型的引用。一般来说,可以认为这个属性用于保存与 fiber 相关的本地状态。
  • type:定义与此 fiber 关联的功能或类。对于类组件,它指向构造函数;对于 DOM 元素,它指定 HTML tag。可以使用这个字段来理解 fiber 节点与哪个元素相关。
  • tag:定义 fiber 的类型。它在 reconcile 算法中用于确定需要完成的工作。如前所述,工作取决于 React 元素的类型,函数 createFiberFromTypeAndProps 将 React 元素映射到相应的 fiber 节点类型。在我们的应用程序中,ClickCounter 组件的属性标记是 1,表示 ClassComponent,而 span 元素的属性标记是 5,表示 Host Component。
  • updateQueue:用于状态更新,回调函数,DOM 更新的队列
  • memoizedState:用于创建输出的 fiber 状态。处理更新时,它会反映当前在屏幕上呈现的状态。
  • memoizedProps:在前一次渲染期间用于创建输出的 props
  • pendingProps:已从 React 元素中的新数据更新,并且需要应用于子组件或 DOM 元素的 props
  • key:具有一组 children 的唯一标识符,可帮助 React 确定哪些项已更改,已添加或从列表中删除。它与此处描述的 React 的 “list and key” 功能有关。

可以在此处找到 fiber 节点的完整结构。在上面的解释中省略了一堆字段,尤其跳过了 child,sibling 和 return,组成了树数据结构。以及特定于 Scheduler 的 expirationTime,childExpirationTime 和 mode 等字段类别。

General algorithm

React 把一次渲染分为两个阶段:render 和 commit。

在 render 阶段时,React 通过 setState 或 React.render 来执行组件的更新,并确定需要在 UI 中更新的内容。如果是第一次渲染,React 会为 render 方法返回的每个元素,创建一个新的 fiber 节点。在接下来的更新中,将重用和更新现有 React 元素的 fiber 节点。render 阶段的结果是生成一个部分节点标记了 side effects 的 fiber 节点树,side effects 描述了在下一个 commit 阶段需要完成的工作。在此阶段,React 采用标有 side effects 的 fiber 树并将其应用于实例。它遍历 side effects 列表并执行 DOM 更新和用户可见的其他更改。

一个很重要的点是,render 阶段可以异步执行。 React 可以根据可用时间来处理一个或多个 fiber 节点,然后停止已完成的工作,并让出调度权来处理某些事件。然后它从它停止的地方继续。但有时候,它可能需要丢弃完成的工作并再次从头。由于在 render 阶段执行的工作不会导致任何用户可见的更改(如 DOM 更新),因此这些暂停是不会有问题的。相反,在接下来的 commit 阶段始终是同步的,这是因为在此阶段执行的工作,将会生成用户可见的变化,例如, DOM 更新,这就是 React 需要一次完成它们的原因。

调用生命周期方法是 React 执行的一种工作。在 render 阶段调用某些方法,在 commit 阶段调用其他方法。在 render 阶段时调用的生命周期列表如下:

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

可以看到,在 render 阶段执行的一些遗留生命周期方法在 react 16.3 中标记为 UNSAFE。它们现在在文档中称为遗留生命周期,将在未来的 16.x 版本中弃用,而没有 UNSAFE 前缀的版本将在 17.0 中删除。可以在此处详细了解这些更改以及建议的迁移路径

为什么会废弃这些声明周期函数呢呢?

因为在 render 阶段不会产生像 DOM 更新这样的副作用,所以 React 可以异步处理与组件异步的更新(甚至可能在多个线程中执行)。然而,标有 UNSAFE 的生命周期经常被误解和滥用,开发人员倾向于将带有副作用的代码放在这些方法中,这可能会导致新的异步渲染方法出现问题。虽然只有没有 UNSAFE 前缀的副本会被删除,但它们仍然可能在即将出现的 concurrent 模式中引起问题。

以下是 commit 阶段执行的生命周期方法列表:

  • getSnapshotBeforeUpdate
  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount

因为这些方法在同步 commit 阶段执行,所以它们可能包含副作用并获取 DOM。

Render 阶段

reconciliation 算法始终使用 renderRoot 函数从最顶端的 HostRoot fiber 节点开始。但是,React 会跳过已经处理过的 fiber 节点,直到找到未完成工作的节点。例如,如果在组件树中调用 setState,则 React 将从顶部开始,但会快速跳过父节点,直到它到达调用了 setState 方法的组件。

Main steps of the work loop

所有 fiber 节点都在 work loop 中处理。这是循环的同步部分的实现

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


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


在上面的代码中,nextUnitOfWork 从 workInProgress 树中保存对 fiber 节点 (这些节点有部分任务要处理) 的引用。当 React 遍历 Fibers 树时,它使用此变量来知道是否有任何其他 fiber 节点具有未完成的工作。处理当前 fiber 后,变量将包含对树中下一个 fiber 节点的引用或 null。在这种情况下,React 退出工作循环并准备提交更改.

有 4 个主要功能用于遍历树并启动或完成工作:

要演示如何使用它们,请查看以下遍历 fiber 树的动画。已经在演示中使用了这些函数的简化实现。每个函数都需要一个 fiber 节点进行处理,当 React 从树上下来时,可以看到当前活动的 fiber 节点发生了变化,可以清楚地看到算法如何从一个分支转到另一个分支。它首先完成 child 节点的工作,然后转移到 parent 身边.

注意,垂直连接表示 sibling,而弯曲的连接表示 child,例如 b1 没有 child,而 b2 有一个 child c1.

这是视频的链接,您可以在其中暂停播放并检查当前节点和功能状态,可以简单的看到,这里适用的树遍历算法是深度优先搜索 (DFS)

让我们从前两个函数 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;
}


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 需要执行的所有活动。出于演示的目的,我们只需记录 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;
}


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


可以看到函数的重点是一个很大的循环。当 workInProgress 节点没有子节点时,React 会进入此函数。完成当前 fiber 的工作后,它会检查是否有兄弟节点;如果找到,React 退出该函数并返回指向兄弟节点的指针。它将被赋值给 nextUnitOfWork 变量,React 将从这个兄弟开始执行分支的工作。重要的是要理解,在这一点上,React 只完成了前面兄弟姐妹的工作。它尚未完成父节点的工作,只有在完成所有子节点工作后,才能完成父节点和回溯的工作.

从实现中可以看出,performUnitOfWork 和 completeUnitOfWork 主要用于迭代目的,而主要活动则在 beginWork 和 completeWork 函数中进行。在后面的部分,我们将了解当 React 进入 beginWork 和 completeWork 函数时,ClickCounter 组件和 span 节点会发生什么.

Commit phase

该阶段以 completeRoot 函数开始,这是 React 更新 DOM 并调用 mutation 生命周期方法的地方。
当 React 进入这个阶段时,它有 2 棵树和 effects list。第一棵树是 current tree, 表示当前在屏幕上呈现的状态,然后是在渲染阶段构建了一个备用树,它在源代码中称为 finishedWork 或 workInProgress,表示需要在屏幕上反映的状态。此备用树通过子节点和兄弟节点指针来与 current 树类似地链接。
然后,有一个 effects list - 通过 nextEffect 指针链接的,finishedWork 树中节点的子集。请记住,effects list 是 render 阶段运行的结果。render 阶段的重点是确定需要插入,更新或删除哪些节点,以及哪些组件需要调用其生命周期方法,其最终生成了 effects list,也正是在提交阶段迭代的节点集。

出于调试目的,可以通过 fiber root 的 current 属性访 current tree,可以通过 current tree 中 HostFiber 节点的 alternate 属性访问 finishedWork 树。

在提交阶段运行的主要功能是 commitRoot。它会执行以下操作:

  • 在标记了 Snapshot effect 的节点上使用 getSnapshotBeforeUpdate 生命周期方法
  • 在标记了 Deletion effect 的节点上调用 componentWillUnmount 生命周期方法
  • 执行所有 DOM 插入,更新和删除
  • 将 finishedWork 树设置为 current 树
  • 在标记了 Placement effect 的节点上调用 componentDidMount 生命周期方法
  • 在标记了 Update effect 的节点上调用 componentDidUpdate 生命周期方法

在调用 pre-mutation 方法 getSnapshotBeforeUpdate 之后,React 会在树中提交所有 side-effects。它通过两个部分:第一部分执行所有 DOM(Host)插入,更新,删除和 ref 卸载,然后,React 将 finishedWork 树分配给 FiberRoot,将 workInProgress 树标记为 current 树。前面这些都是在 commit 阶段的第一部分完成的,因此在 componentWillUnmount 中指向的仍然是前一个树,但在第二部分之前,因此在 componentDidMount / Update 中指向的是最新的树。在第二部分中,React 调用所有其他生命周期方法和 ref callback, 这些方法将会单独执行,因此已经调用了整个树中的所有放置 (placement),更新和删除.

下面这段代码运行上述步骤的函数的要点,其中 root.current=finishWork 及以前为第一部分,其之后为第二部分.

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


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


这些子函数中的每一个都实现了一个循环,该循环遍历 effects list 并检查 effect 的类型, 当它找到与函数功能相关的 effects 时,就会执行它.

Pre-mutation lifecycle methods

例如,这是在 effect tree 上迭代并检查节点是否具有 Snapshot effect 的代码:

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


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


对于类组件,该 effect 意味着调用 getSnapshotBeforeUpdate 生命周期方法.

DOM updates

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


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


有趣的是,React 调用 componentWillUnmount 方法作为 commitDeletion 函数中删除过程的一部分.

本文转自Richard - [译]深入React fiber架构及源码