深入理解React协调算法

561 阅读19分钟

前言

React是一个用于构建界面的JavaScript库。它的核心是跟踪组件状态变化并将更新后的状态更新到屏幕上。在React中,我们把这个过程称为 reconciliation (协调)。通过调用setState方法,React检查状态或属性是否已更改,并在UI层上更新。

背景介绍

首先看一个简单的例子:

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

1.gif

这是一个简单的计数器的例子, 点击按钮,组件的状态就会在处理程序中更新,组件的状态的更新反过来会引起span元素的内容更新。

下面是在协调阶段,React内部会有各种活动。以下是计数器在协调阶段所做的操作:

  1. 更新state的count属性
  2. 检查并比较ClickCounter的子代以及props
  3. 更新span元素

协调期间还会执行其他活动,例如调用生命周期方法,更新ref。这些活动在Fiber架构中统称为"work"(工作)。work的类型取决于React元素的类型。React元素有多种类型,比如类组件,函数组件,Portals,DOM节点等。而React元素类型则是由React.createElement的第一个参数决定。React.createElement函数在render创建元素中调用。

React.createElement到Fiber

JSX编译

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

JSX在经过编译后,会得到如下的结果。这是render方法真正返回的结果

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

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
        }
    }
]
  • type,key,props,用于描述元素

而对于组件<ClickCounter>的元素,它没有props和key:

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

Fiber节点

在协调期间,render方法返回的React元素会被合并到Fiber节点树之中。每一个React元素都有对应的Fiber节点。与React元素不同,Fiber不会在每一次渲染的时候重新创建。Fiber会保存组件的状态和DOM。

前面讨论过,根据不同的React元素类型,会执行不同的活动。例如,对于Class组件会调用生命周期方法以及render方法。而对于DOM节点,它执行DOM mutation。因此,每个React元素都被转换为相应类型的Fiber节点。节点描述了需要完成的"work"。

可以将Fiber节点看作一种数据解构,表示一个work单元。,Fiber架构还提供了一种跟踪、调度、暂停和中止work的方法。

React会在首次将React元素转换为Fiber节点时,使用createFiberFromTypeAndProps函数创建Fiber节点。在更新阶段会复用Fiber节点,并使用React元素上的数据更新Fiber节点上的属性。亦或者移动,删除Fiber节点。

React源码中的ChildReconciler函数包含了Fiber节点中所有的work

React会为每一个React元素创建一个Fiber节点,我们会得到一个Fiber节点树

2.png

Fiber节点树是通过链表的形式存储的,每一个Fiber都拥有child(第一个子节点的引用),sibling(第一个兄弟节点的引用)和return(父节点的引用),来表示层级关心。更多内容请参考这篇文章React Fiber为什么使用链表来设计组件树

current tree 和 workInProgress tree

在第一次渲染完成后,React会生成一个Fiber树。该树映射了应用程序的状态,这颗树被称为current tree。当应用程序开始更新时,React会构建一个workInProgress tree, workInProgress tree映射了未来的状态。

所有的"work"都是在workInProgress tree上的Fiber节点上进行的。当React开始遍历current tree时,它会为每一个现有的Fiber节点创建一个备份(alternate字段),alternate节点构成了workInProgress tree。当所有更新和相关的"work"完成。workInProgress tree会刷新到屏幕上。workInProgress tree此时变为了current tree

React的核心原则之一是"一致性", 它总是一次性更新DOM, 不会显示部分结果. workInProgress就是一个用户不可见的"草稿", React在它上面处理所有组件, 处理完成后将它再刷新到界面上.

在React的源码中,有很多从workInProgress treecurrent tree中获取Fiber节点的函数。比如下面这个函数签名

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

workInProgress tree的Fiber节点拥有current tree对应节点的引用。反之亦然。

副作用

我们可以将React组件视为使用state和props计算UI的函数。其他的活动,比如手动修改DOM,调用生命周期都应该被视作一种副作用。在React的文档中也提及了这一点

你之前可能已经在 React 组件中执行过数据获取、订阅或者手动修改过 DOM。我们统一把这些操作称为“副作用”(side-effects),或者简称为“作用”(effects)。因为它们会影响其他组件,并且在渲染期间无法完成。

大多数state和props的更新都会导致副作用。应用effects是一种work类型。因此Fiber节点是一种跟踪更新和effects的便捷机制,每一个Fiber节点都有与之相关联的effects。它们被编码在effectTag字段之中

Fiber中的effects定义了处理更新之后需要做的"work"。对于DOM元素,"work"包含了添加,更新,删除。对于类组件,包括了更新ref,调用componentDidMount和componentDidUpdate生命周期方法。还有其他effects对应于其他类型的Fibber。

Effects list

React处理更新非常快。为了达到更好的性能水平,采用了一些有趣的技术。其中之一就是将具有effects的Fiber节点,构建为线性列表,以方便快速迭代。迭代线性列表要比迭代树快的多,因为不需要迭代没有side-effects的节点。

effects list的目的是是标记出具有DOM更新,或其它与之关联的其他effects的节点。effects list是finishedWork树的子集。在workInProgress treecurrent tree中使用nextEffect属性链接在一起。

丹·阿布拉莫夫(Dan Abramov)将Effects list提供了一个比喻。将Fiber想象成一颗圣诞树,用圣诞灯将所有有效的节点连接在一起。

为了可视化这一点,让我们想象下面的Fiber树,其中高亮显示的节点有一些“work”要做。

例如,我们的更新导致将c2插入到DOM中,d2和c1更改属性,b2触发生命周期方法。 Effects list列表将把它们链接在一起,这样React就可以遍历时跳过其他节点。

3.png

可以看到具有effects的节点如何链接在一起。当遍历节点时,React使用firstEffect指针确定列表的起始位置。上图的Effects list可以用下图表示

4.png

Fiber节点树的根

React应用都有一个或者多个充当容器的DOM元素

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

React会为容器创建FiberRoot对象,可以使用容器的DOM引用访问Fiber root对象:

// Fiber root对象
const fiberRoot = query('#container')._reactRootContainer._internalRoot

Fiber root是React保留对Fiber树引用的地方,Fiber树存储在Fiber root对象的current属性中

// Fiber树
const hostRootFiberNode = fiberRoot.current

Fiber树的第一个节点是一种特殊的类型节点,叫做HostRoot。它在内部创建,是最顶层组件的父组件。通过HostRoot节点的stateNode属性可以访问FiberRoot节点.

// Fiber root对象
const fiberRoot = query('#container')._reactRootContainer._internalRoot
// hostRoot
const hostRootFiberNode = fiberRoot.current
// true
hostRootFiberNode.stateNode === fiberRoot

我们可以从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元素的Fiber节点结构:

{
    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(记录与之关联的effects), nextEffect(连接具有effects的Fiber节点使其成为线性节点)

stateNode属性

保留对class组件实例的引用, DOM节点或其他与Fiber节点相关联的React元素类实例的引用。一般来说, 我们可以说这个属性被用于保存与当前Fiber相关的本地状态。

type属性

定义与此Fiber节点相关联的函数或者类。对于class组件,type属性指向构造函数。对于DOM元素,type属性指向HTML标记。我经常用这个字段来判断这个Fiber节点与那个元素相关。

tag属性

定义Fiber节点的类型。在协调期间使用它确定需要做的"work"。如之前所述"work"取决于React元素的类型。createFiberFromTypeAndProps函数将React元素映射成相对应的Fiber节点类型。

在我们的例子中。ClickCounter的tag为1,表示为ClassComponent。span的tag为5,标记为HostComponent。

updateQueue属性

state更新和回调,DOM更新的队列。

memoizedState属性

用于创建输出Fiber的state。在处理更新的时候,它映射的是当前界面上呈现的state。

memoizedProps属性

在上一次渲染过程中用来创建输出的Fiber props。

pendingProps属性

已经更新后的Fiber props。需要用于子组件和DOM元素。

key属性

一组children中的唯一标示。帮助React确定那些发生了更改,新增或删除。更详细的解释在这里

总结

完整的Fiber结构, 可以在这里看到,在上面的说明省略了很多的字段比如child,sibling并return。这三个字段是构成链表树结构的关键。以及expirationTime、childExpirationTime和mode,这些字段是特定于Scheduler的。

通用算法

React分两个阶段执行work:render(渲染)和 commit(提交)

在render(渲染)阶段,React将更新应用于通过setState或React.render调度的组件, 并找出需要在UI中更新的内容。

如果是初始渲染,React将为render方法返回的每一个元素创建新的Fiber节点。在之后的更新中,将重新使用和更新现有的Fiber节点。

render阶段会构建一个带有side-effects(副作用)的Fiber节点树。effects描述了下一个commit(提交)阶段需要完成的“work”。在commit(提交)阶段,React会使用标记有effects的Fiber节点并将其应用于实例上。遍历Effects list执行DOM更新和其他对用户可见的更改。

请切记,render阶段的工作是可以异步执行的,React根据可用时间处理一个或者多个Fiber节点。当发生一些更重要的事情时,React会停止并保存已完成的工作。等重要的事情处理完成后,React从中断处继续完成工作。但是有时可能会放弃已经完成的工作,从顶层重新开始。此阶段执行的工作是对用户是不可见的,因此可以实现暂停。但是在commit(提交)阶段始终是同步的它会产生用户可见的变化, 例如DOM的修改. 这就是React需要一次性完成它们的原因。

调用生命周期函数使用React的“work”之一。在render阶段调用这些生命周期方法:

  • UNSAFE_componentWillMount (deprecated)
  • UNSAFE_componentWillReceiveProps (deprecated)
  • getDerivedStateFromProps
  • shouldComponentUpdate
  • UNSAFE_componentWillUpdate (deprecated)
  • render

由于render阶段不会产生DOM更新之类的副作用,因此React可以异步地对组件进行异步处理更新(甚至可能在多个线程中进行)。但是带有UNSAFE_前缀的生命周期函数常常会被误用,开发者会把副作用添加到这些生命周期函数中。这可能会导致异步渲染出现问题

在commit阶段调用这些生命周期方法,这些生命周期方法在commit阶段执行,所以它们可能包含副作用并涉及DOM更新。

  • getSnapshotBeforeUpdate
  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount

渲染(render)阶段

协调算法使用renderRoot函数从最顶层的HostRoot节点开始,跳过已经处理过的节点,直到找到work未完成的节点为止。例如, 当在组件树深处调用setState方法, React 从顶部开始快速的跳过所有父级节点直接获得调用setState方法的组件。

WorkLoop

所有的Fiber节点在render阶段都会在WorkLoop中被处理。这是循环同步部分的实现:

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

在上面的代码中,nextUnitOfWork保持了对workInProgress tree中一个有工作要处理的Fiber节点的引用。在React遍历Fiber树时,会使用nextUnitOfWork判断是否有未完成"work"的Fiber节点。当节点处理完成“work”后,nextUnitOfWork会指向下一个Fiber节点的引用或者为null。当nextUnitOfWork为null时,React会退出WorkLoop,并准备进入到commit阶段。

有四个主要的方法用于遍历树,并启动或完成工作:

为了演示如何使用它们。请查看下面遍历Fiber树的演示动画。演示动画中使用了这些函数的简化实现。我们可以通过演示看到,首先处理子节点的“work”,然后处理父节点的“work”。

5.gif

使用直线连接代表同级,使用折线连接的代笔子级

逐步拆分下React遍历Fiber树的过程(首先处理子节点的“work”,然后处理父节点的“work”):

  1. beginWork a1
  2. beginWork b1,completeWork b1
  3. beginWork b2
  4. beginWork c1
  5. beginWork d1, completeWork d1
  6. beginWork d2, completeWork d2
  7. completeWork c1
  8. completeWork b2
  9. beginWork b3
  10. beginWork c2,completeWork c2
  11. completeWork b3
  12. completeWork a1

这个是视频的连接, 从概念上将"begin"看成进入组件,“complete”看成离开组件。

我们首先看下beginWork和performUnitOfWork这两个函数:

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 tree中接收一个Fiber节点。然后调用beginWork开始处理Fiber节点的work。为了演示,这里只是log了Fiber节点的name字段表示work已经完成。函数beginWork总是返回指向循环中下一个子节点或null。

如果有下一个子节点, 它将在workLoop函数中分配给nextUnitOfWork。如果没有子节点,React就知道了到达了分支的结尾。就会完成当前Fiber节点的work。React会执行它兄弟节点的工作,最后回溯到父节点。这是在completeUnitOfWork中完成的。

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

        nextUnitOfWork = completeWork(workInProgress);

        if (siblingFiber !== null) {
            // 如果有同级,则返回它。以继续执行同级的工作
            return siblingFiber;
        } else if (returnFiber !== null) {
            // 回溯到上一级
            workInProgress = returnFiber;
            continue;
        } else {
            // 已经到了root节点
            return null;
        }
    }
}

function completeWork(workInProgress) {
    console.log('work completed for ' + workInProgress.name);
    return null;
}

当workInProgress节点没有子节点时,会进入此函数。在完成当前Fiber的工作后,会检查是否有兄弟节点。如果有,返回同级的兄弟节点的指针,分配给nextUnitOfWork。React将会从兄弟节点开始工作。只有处理完子节点所有分支之后, 才会回溯到父节点(所有子节点处理完成后,才会回溯到父节点)。

从实现可以看出,completeUnitOfWork主要用于迭代,主要工作都是beginWork和completeWork函数中进行的。

完整的示例

这里是完整的示例(beginWork,performUnitOfWork,completeUnitOfWork,completeWork的简易实现)

// 首先构建链表树
const a1 = {name: 'a1', child: null, sibling: null, return: null};
const b1 = {name: 'b1', child: null, sibling: null, return: null};
const b2 = {name: 'b2', child: null, sibling: null, return: null};
const b3 = {name: 'b3', child: null, sibling: null, return: null};
const c1 = {name: 'c1', child: null, sibling: null, return: null};
const c2 = {name: 'c2', child: null, sibling: null, return: null};
const d1 = {name: 'd1', child: null, sibling: null, return: null};
const d2 = {name: 'd2', child: null, sibling: null, return: null};
a1.child = b1;
b1.sibling = b2;
b2.sibling = b3;
b2.child = c1;
b3.child = c2;
c1.child = d1;
d1.sibling = d2;
b1.return = b2.return = b3.return = a1;
c1.return = b2;
d1.return = d2.return = c1;
c2.return = b3;

// 当前的指针是a1
let nextUnitOfWork = a1;
workLoop();

// 开始工作循环
function workLoop() {
    while (nextUnitOfWork !== null) {
        nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    }
}

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

function beginWork(workInProgress) {
    log('work performed for ' + workInProgress.name);
    return workInProgress.child;
}

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

        nextUnitOfWork = completeWork(workInProgress);

        if (siblingFiber !== null) {
            return siblingFiber;
        } else if (returnFiber !== null) {
            workInProgress = returnFiber;
            continue;
        } else {
            return null;
        }
    }
}

function completeWork(workInProgress) {
    log('work completed for ' + workInProgress.name);
    return null;
}

function log(message) {
  let node = document.createElement('div');
  node.textContent = message;
  document.body.appendChild(node);
}

提交(commit)阶段

提交阶段从completeRoot开始。这是React更新DOM,调用getSnapshotBeforeUpdate,componentDidMount,componentDidUpdate,componentWillUnmount等生命周期的地方。

React进入这一阶段时,有两颗树(workInProgress tree和current tree)以及effects list。current tree表示了当前屏幕上呈现的状态。render阶段遍历current tree时会生成另一颗树,在源码中被称为finishWork或workInProgress,表示未来需要在屏幕上呈现的状态。workInProgress treecurrent tree结构类似。

调试时,如何获取current tree以及workInProgress tree?

// current tree
// 从容器对象上获取FiberRoot对象
const fiberRoot = query('#container')._reactRootContainer._internalRoot
// 获取current tree
const currentTree = fiberRoot.current
// 获取workInProgress tree
const workInProgressTree = fiberRoot.current.alternate

提交(commit)阶段,主要执行commitRoot函数,执行以下的操作:

  1. 在有Snapshot标记的Fiber节点上调用getSnapshotBeforeUpdate生命周期方法。
  2. 在有Deletion标记的Fiber节点上调用componentWillUnmount生命周期方法。
  3. 执行所有的 DOM 插入, 更新, 删除。
  4. workInProgress tree设置为current tree
  5. 在有Placement标记的Fiber节点上调用componentDidMount生命周期方法。
  6. 在有Update标记的Fiber节点上调用componentDidUpdate生命周期方法。

在调用getSnapshotBeforeUpdate方法后,React将commit,Fiber树中所有的副作用。分为两步:

第一步,执行所有的DOM插入,更新,删除和ref卸载。然后将workInProgress tree设置为current tree树。这是在第一步完成之后,第二步之前完成的。因此在componentWillUnmount生命周期方法在执行期间,状态依然是更新之前的。而componentDidMount/componentDidUpdate执行时的状态是更新之后的。第二步,执行其他生命周期方法和ref回调,这些方法作为单独的过程被执行。

commitRoot方法的预览:

function commitRoot(root, finishedWork) {
    // 用来执行getSnapshotBeforeUpdate
    commitBeforeMutationLifecycles()
    // 用户更新DOM,以及执行componentWillUnmount
    commitAllHostEffects();
    root.current = finishedWork;
    // 调用componentDidUpdate和componentDidMount生命周期的地方
    commitAllLifeCycles();
}

这些子函数,内部都包含了一个循环。循环遍历effects list,并检查effects的类型。当发现类型和子函数的目的相同时,就应用它。

Pre-mutation lifecycle methods

遍历effects list,并检查节点是否具有Snapshot effect的源代码:

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

如果是class组件,调用getSnapshotBeforeUpdate生命周期方法。

DOM updates

commitAllHostEffects是React执行DOM更新的地方。React会把componentWillUnmount作为commitDeletion删除过程中的一部分。

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

Post-mutation lifecycle methods

commitAllLifecycles是React调用所有剩余生命周期方法componentDidUpdate和componentDidMount的地方。

总结

React源码很复杂,Max Koretskyi的这篇文章内容也很多,所有总结下这篇博客的要点:

  1. React的核心是跟踪组件状态变化并将更新后的状态更新到屏幕上。在React中,我们把这个过程称为 reconciliation (协调)。
  2. 协调期间的各种活动在Fiber架构中统称为work(工作),work的类型取决于React元素的类型,React元素的类型取决于React.createElement函数的第一个参数类型。
  3. 协调期间React元素会被合并到Fiber节点树之中,每一种React元素都会一种对应的Fiber节点。Fiber节点不会重复创建,会在第一次渲染后重用。
  4. React会为每一个React元素创建一个Fiber节点,我们会得到一个Fiber节点树。Fiber节点树是通过链表的形式存储的。每一个Fiber节点都拥有child,sibling和return字段,用于中断恢复遍历。
  5. 在初次渲染时会生成一棵Fiber树。被称为current tree。当开始更新时,React会构建一个workInProgress tree
  6. current tree代表了当前的状态,workInProgress tree代表了未来的状态。
  7. 在commit阶段workInProgress tree会被设置为current tree
  8. React在遍历current tree时,会为每一个Fiber节点创建一个alternate字段,alternate字段保存了Fiber节点的备份,alternate字段上保存的备份Fiber节点构成了workInProgress tree。所有的工作都是在workInProgress tree上进行的。
  9. 数据获取、订阅或者手动修改过DOM都统称为副作用(side-effects)或者简称为“作用”(effects)。因为它们会影响其他组件,并且在渲染期间无法完成。
  10. effects是一种work类型。因此Fiber节点是一种跟踪更新和effects的便捷机制,每一个Fiber节点都有与之相关联的effects。它们被编码在effectTag字段之中。Fiber中的effects定义了处理更新之后需要做的"work"。对于DOM元素,"work"包含了添加,更新,删除。对于类组件,包括了更新ref,调用componentDidMount和componentDidUpdate生命周期方法。还有其他effects对应于其他类型的Fiber。
  11. React会将具有effects的Fiber节点,构建为线性列表,以方便快速迭代。firstEffect是列表的开始位置,使用nextEffect属性将节点链接在一起(构建成线性列表)。
  12. 可以通过容器DOM元素,获取FiberRoot对象query('#container')._reactRootContainer._internalRoot
  13. FiberRoot对象的current属性,是current tree的第一个节点,被称为hostRoot。fiberRoot.current
  14. hostRoot节点的stateNode属性,指向FiberRoot对象。
  15. 如何获取workInProgress tree, 答:fiberRoot.current.alternate
  16. 如何获取current tree, 答: fiberRoot.current
  17. Fiber节点结构见上文。
  18. React分两个阶段执行work:render(渲染)和 commit(提交)
  19. render阶段的工作是可以异步执行的,React根据可用时间处理一个或者多个Fiber节点。当发生一些更重要的事情时,React会停止并保存已完成的工作。等重要的事情处理完成后,React从中断处继续完成工作。但是有时可能会放弃已经完成的工作,从顶层重新开始。此阶段执行的工作是对用户是不可见的,因此可以实现暂停。
  20. commit(提交)阶段始终是同步的它会产生用户可见的变化, 例如DOM的修改. 这就是 React需要一次性完成它们的原因。
  21. 所有的Fiber节点在render阶段都会在WorkLoop中被处理。WorkLoop中主要有四个函数,performUnitOfWork,beginWork,completeUnitOfWork,completeWork。WorkLoop首先处理子节点的“work”,所有子节点处理完成后,回溯然后处理父节点的“work”。从概念上将"begin"看成进入组件,“complete”看成离开组件。
  22. 提交(commit)阶段分为两步实现。第一步,执行所有的DOM插入,更新,删除和ref卸载以及执行componentWillUnmount生命周期方法。第二步,执行其他生命周期方法和ref回调,这些方法作为单独的过程被执行。第一步和第二步中间会将workInProgress tree设置为current tree树。

参考