2020年5月

基于这篇文章学习讨论Fiber,基本为翻译内容

Dive deep into React's new architecture called Fiber and learn about two main phases of the new reconciliation algorithm. We'll take a detailed look at how React updates state and props and processes children.

React是一个用于构建用户界面的JavaScript库。核心机制(change detection有专门的文章介绍)是追踪(tracks)组件状态变化并将其更新到显示界面上。React中这个过程通常被称为“协调”(reconciliation),调用setState 方法,框架检查state或者props是否变更,然后将组件重新渲染到UI。

React文档提供了一篇文章,从较高的层面概述了这种机制:React元素的地位,生命周期方法与render方法,以及diffing算法如何应用到子组件。render方法返回一颗不可变(immutable)的,由React元素组成的树,也就是俗称的“虚拟DOM”(之前的文章也提到过,React不再使用“虚拟DOM”这个概念)。这一术语早期被用来辅助解释说明React体系,但也引起一些困惑,现在已不再使用。下文将使用“React元素树”来表达。

除了React元素树,框架也总持有一个由内部实例(组件,DOM节点等)构成的树,用于保存状态。从v16开始,React推出了Fiber—重新实现了内部实例树的组成,以及相应的管理算法。了解Fiber架构带来的优点,请查阅The how and why on React’s usage of linked list in Fiber这篇文章(稍后再来学习这篇文章)。

本文是系列文章的第一篇,目标是介绍React的内部结构。这篇文章将提供一个深度的概述,包括重要的概念和算法相关的数据结构。当有了足够的背景知识,再讨论具体算法和用于遍历处理fiber树的主要函数。系列的下一篇文章会演示(demonstrate)React如何使用算法执(perform)行初始化渲染,并处理state和props更新,从中将深入到调度器的细节,子协调进程,和构建副作用列表的机制。

先介绍一些高级知识,强烈建议阅读,理解并发(Concurrent)React内部工作背后的技巧。如果你希望参与到React的开发,本系列文章将给你提供很好的指引。作者是一个逆向工程师,所以本文会有很多链接到新版本(v16.6.0)源码的内容。

吸收理解肯定吃力,但是不要觉得有压力。花费的精力肯定是值得的。注意:你不需要了解本文内容也可以使用React,本文是关于React内部的工作机制。

Setting the background 背景知识

一个简单的计数器应用:

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方法返回这个简单的组件,包含两个子元素buttonspan。一点按钮,组件的状态就在内部处理器被更新。反过来,结果以文本的形式更新到 span元素内。

协调过程中React执行了很多动作。比如,第一次渲染过程以及状态更新后,执行了很多高级操作:

  • 更新ClickCounter组件状态state中的 count属性
  • 检索(retrieves)并比较ClickCounter的子组件及其props
  • 更新span元素的props(innerHtml算作span元素的props)

当然在协调过程中还有其他的动作(activities)被执行,比如生命周期或者引用(refs)更新。所有这些动作,在Fiber架构中,被统一称为“work”(暂时译作“作业”)。作业的类型通常依赖于React元素的类型,比如对于类组件来说,React要创建一个实例,函数组件不需要。如你所知,React元素有很多种,类组件、函数组件以及宿主组件(host components,浏览器环境对应DOM节点)以及传送门(portals)等。React元素的类型有createElement函数的第一个参数决定。这个函数通常用于render方法中创建元素。

在开始探究fiber主要算法之前,先熟悉一下内部使用的数据结构。

From React Elements to Fiber nodes 从React元素到Fiber节点

React中每一个组件都代表一个具体的UI,通常称为“视图”或者“有render方法返回的模板”。上面的例子转换成模板可以表达为:

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

React Elements React元素

当模板被JSX编译器处理后,会得到一簇(a bunch of)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元素。然后是typekeyprops 字段用于描述组件的实际情况,具体值由React.createElement函数参数决定。React将文本内容视为spanbutton节点的子节点(children)。然后点击处理器是button元素props的一部分。React元素还有一些其他的字段不在本文讨论的内容,比如ref

ClickCounter组件本身对应的React元素没有props(从组件声明上可以看出并没有接受显式的props)和key值:

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

Fiber nodes Fiber节点

协调过程中,每一个React元素render方法返回的数据,被合并(merged into)到fiber节点的树。每一个React元素都有一个对应的fiber节点。不像React元素,fiber们不会在每次渲染时重新创建。这些可变数据结构持有组件状态和DOM。

之前讨论到,React依据元素类型执行不同的动作。在之前的例子中,对于ClickCounter类组件,会调用其生命周期方法以及render方法,而对span这样的宿主元素(浏览器环境下DOM节点),将执行DOM操作。所以每个React元素被转化为对应类型的Fiber节点,这个类型就决定了需要完成的作业。

你可以将fiber想成这样的数据结构:代表了一些需要执行的作业,或者说一个作业单元(a unit of work),也就是作业的基本单位。Fiber架构提供了方便的方式进行追踪、调度、暂停和放弃。

当一个React元素一开始被转化为fiber节点,React在createFiberFromTypeAndProps函数中,使用元素的数据创建一个fiber。在随后的(consequent)更新中,React重用fiber节点,使用对应的React元素的数据,只更新必须的属性。在key属性基础上,同层次可能只需要移动节点,或者对应的React元素不再返回数据时删除节点。

可以查看ChildReconciler函数,包含了具体的动作(activities)以及相应的函数。这里简单的列举一些,源码可以查看这里

deleteChild,deleteRemainingChildren,mapRemainingChildren,placeChild,placeSingleChild,updateTextNode,updateElement,updatePortal,updateFragment,createChild,updateSlot,updateFromMap等。

React为每一个React元素创建fiber,而React元素以树型结构相互关联,因此也会得到一个fiber节点构成的树。在ClickCounter的例子中,就会得到如下的树形关系:

https://i.loli.net/2020/05/11/DEXPLgfJ7xM1kWZ.png

由于ClickCounter也会被渲染到某个节点,所以这里用HostRoot来代替。所有的fiber节点,通过childsiblingreturn属性相互联系,形成一个链表结构。更多细节请查看The how and why on React’s usage of linked list in Fiber这篇文章(后续应该也会翻译)。

Current and work in progress trees 当前和处理中树

首次渲染后,React得到一个fiber树,映射了应用用于渲染视图的状态。这棵树通常被称为current当前树。当React开始处理更新时,还会创建一个workInProgress树,这棵树映射了将来要渲染(flushed)到页面的状态。

所有作业被执行于workInProgress树的fibers。React遍历current树,对每一个存在的fiber节点都会创建一个备用(alternate)节点,由这些备用节点构成workInProgress树。这样的节点也是由React元素render方法返回的数据构造的。一旦更新被处理,且所有相关工作都完成了,React将得到一个备用(alternate)树,准备渲染到屏幕上。当这个workInProgress 树被渲染到屏幕上,它就成了current树。

React的一个核心原则就是一致性(consistency)。React更新DOM总是一气呵成(in one go)—也就是不会展示部分(partial)结果。那么workInProgress树就像一张对用户不可见的草图(draft),所以React首先会处理所有的组件,然后在将整体的变更展示出来。

在上一节引用ChildReconciler的源码中,有很多代表动作的函数,它们接受的fiber节点参数可以来自current树也可以来自workInProgress树。比如:

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

每一个fiber节点,都会通过alternate字段来持有一个其”替身“的引用,current树的节点通过alternate字段持有workInProgress树中对应节点的引用,反之亦然(vice versa)。

Side-effects 副作用

React中的组件可以想象成是一个函数,利用state和props来计算UI展示的方式。所有其他行为,比如DOM变更或者调用生命周期方法,都被认为是一个副作用,或者简单的叫它影响(effect)。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.

你可能会执行数据获取操作,订阅操作或者在React之前操作DOM。这些操作都被称为副作用,因为它们会影响其他组件,并且不能在渲染过程中完成。

你会发现大部分state和props更新都将引发副作用。并且因为践行(applying)副作用也是作业的一种,一个fiber节点,就成为一种很方便的机制,来追踪除了更新意外的副作用。每个fiber节点可以拥有其相关联的副作用,它们以编码的形式,通过effectTag字段来体现。

所以副作用在Fiber体系中,基本上定义了那些在更新被处理完之后,实例应当完成的作业。SideEffectTag是一个特殊的数值,在源码中有体现,比如没有副作用NoEffect用0b00000000000也就是0来表示。对于宿主组件,浏览器环境来说就是DOM元素,由增加、更新和移除元素构成作业。对于类组件,React可能需要更新refs并调用componentDidMount和componentDidUpdate生命周期方法。当然还有其他副作用,分别有不同类型的fiber与之对应。

Effects list (暂不知道怎么表达)

React处理更新非常迅速,为了达到这么优秀的性能,使用了一些非常有趣的技术。其中之一就是为了快速迭代,构建了一个线性表,其中的元素都是因为状态更新而收到影响的fiber节点(原文为:fiber node with effects)。遍历线性表要比遍历树快很多,而且也没有必要在那些没有副作用的节点上。

这个线性表的目标,是标记那些有DOM更新或者其他与之关联的节点。这个列表是finishedWork树的子集,通过nextEffect属性链接起来,并不是current树和workInProgress树中的child属性。

Dan Abramov有这样一个关于effects list的比喻。他将其想成一棵圣诞树,所有effectful节点都绑着圣诞彩灯。为了可视化理解,想象下面这样的树,其中高亮的节点表示有些工作要做。比如,更新导致c2被插入到DOM中,d2c1变更属性,b2触发一个生命周期。effect list会将这些节点连起来,React就能跳过其他的节点了。

https://i.loli.net/2020/05/13/AbQy4W5jt9h36Vx.png

可以看出受影响的节点是如何连接在一起的。当要重新处理这些节点时,React通过firstEffect指针来确定从哪里开始。所以下面的图可以简述这一线性表的结构:

https://i.loli.net/2020/05/13/EXchFmDflHuG2iI.png

Root of the fiber tree fiber树的根节点

每个React应用都有一个或多个像容器一样的DOM元素。也就是ReactDOM.render函数的第二个参数,应当是一个DOM元素:

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

React为每一个这样的容器创建一个fiber根对象

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

没错,你没有看错,是直接挂在了DOM元素上!

React通过fiber根对象持有整个fiber树的引用,这个引用保存在fiber根对象的current属性。

const hostRootFiberNode = fiberRoot.current

fiber树开始于一个特殊类型(HostRoot=3)的fiber节点—HostRoot。它是内部创建的,并且作为最顶层组件的父级。从HostRootFiberRoot通过stateNode属性有一个反向的连接关系:

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

通过fiber根访问顶层HostRoot fiber节点,从而暴露整个fiber树。或者你可以从组件实例获取私有节点:

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, // 对应ClassComponent
    effectTag: 0,
    nextEffect: null
}

组件包含的span元素也有对应的fiber节点(因为其innerHTML属性依赖应用状态):

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

fiber节点还是有很多字段的,其中alternateeffectTagnextEffect之前提到过,分别是current树和workInProgress树的连接点,类型和effect链表的指针。下面介绍一下其他一些重要的属性。

这里插播一丢丢小内容:

随便打开一个由React编写的站点(比如redux官方文档),打开控制台,如果安装了React调试插件,在Components标签页找一个组件,再到Elements标签页找到对应的DOM元素,邮件保存为全局临时变量temp1,然后你可以看到这样的内容:

https://i.loli.net/2020/05/13/AvOz4m1RfSlL87W.png

插播结束。

stateNode

持有组件类实例的引用,是一个DOM元素或者与当前fiber节点相关联的其他React元素。一般来说这个字段用于保存于fiber相关的本地状态。

type

用一个字符串(DOM元素的HTML标签名)或者函数(类组件的构造函数,函数组件的本体)标识fiber的类型(这个之前应该提到过)。这个字段经常用来直接明确fiber对应的具体元素。

tag

由一组枚举值标识fiber的类型。该值用于协调算法,决定对其执行何种操作。之前提到过,作业分类依赖React元素类型。函数createFiberFromTypeAndProps把一个React元素映射成对应类型的fiber节点。在应用中,ClickCounter组件的tag属性是1,代表是CLassComponent,而span的tag是5代表是HostComponent。

updateQueue

一个包含状态更新,回调和DOM更新的队列

memoizedState

之前用来创建输出的状态。处理更新时(也就是新状态到来时),这个字段反映了当前正在使用的状态(也就是相对来说的旧状态)。

memoizedProps

基本同上

pendingProps

已经被更新,但是还需要应用到子组件或子元素的Props,可以理解为没有完全生效的props。

key

用于标识一组子元素的唯一标识符,让React可以在列表中找到具体增删改的那一个。这与React原文档中”列表和key“一章直接相关。

完整的fiber节点结构可以查看这里。上文忽略了大部分字段,尤其跳过了childsiblingreturn这三个指针,而这三个指针正是构成树形结构的关键。并且有一类字段,比如expirationTimechildExpirationTimemode,这些和调度器相关。

General algorithm 广义算法

React在两个主要阶段执行作业:rendercommit

在首次渲染阶段,React将更新应用到那些调用了setState或者React.render的组件,并找出哪些在UI上需要作出更改。如果是初始化渲染,React将为render方法返回的元素创建一个新的fiber节点。在后续的更新中,存续的React元素的fibers会被重用并更新。渲染阶段的结果就是一颗有fiber节点构成的树,并且每个节点可能被标记有副作用。effects描述了那些在接下来的提交阶段(commit phase)需要完成的工作。在这一阶段,React拿到带有effects的fiber树,然后将effects作用到对应的实例上。在遍历effects列表的过程中,执行DOM更新和其他对用户可见的更新。

首次渲染过程中需要完成的作业,是可以被异步执行的,了解这一点非常重要。取决于可用时间,React可以处理一个或多个fiber节点,然后停下来保留完成的作业,将执行权交出(yield)到其他事件。之后还会从停下来的地方继续执行作业。尽管有些时候,可能需要丢弃已经完成的作业再从头开始。这些中断是可行的,因为这个阶段执行的作业不会导致像DOM更新这样用户可见的变更。相反的,接下来的commit阶段总是同步的。这是因为这个阶段执行的作业,总是那些会导致用户可见内容的变更。这也是为什么React需要一次性完成这些操作。

调用声明周期方法是React可能执行的一种作业。一些方法在render阶段被调用,也有的在commit阶段调用。下面是首次render阶段可被调用的生命周期:

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

你是否好奇为什么这些钩子不安全甚至将要被移除?

刚才讨论过渲染阶段不产生诸如DOM更新的副作用,React可以异步的处理更新(甚至可以在多线程中进行)。但是那些带有UNSAFE标记的生命周期,总是被误解,被错误的使用。在新的异步渲染模式中,开发者倾向于将带有副作用的代码放在那些可能引起问题的方法中。虽然只有UNSAFE前缀的副本会被移除,但是他们在将来的并发模式中可能产生问题。

commit阶段会被执行的生命周期:

  • getSnapshotBeforeUpdate
  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount

这些方法在一个同步的commit阶段执行,他们可以包含副作用并接触到DOM。

那么现在了解了一些关于遍历树和执行作业的广义算法的背景知识,下面要进行深入了解。

Render phase 渲染阶段

调度算法总是通过renderRoot函数,从最顶层的HostRoot fiber节点开始。React摆脱/跳过(bails out of/skips)那些已经处理过的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 {...}
}

上面的代码中,nextUnitOfWork持有一个fiber节点的引用,这个节点来自于workInProgress树,并且有要执行的作业。当React遍历Fiber树时,使用这个变量获知现在是否有未完成的作业。在当前fiber节点被处理完之后,这个变量要么变成树中下一个节点的引用,要么是null。如果是null的话就会退出作业循环,然后准备好提交变更。

有四个主要的函数,被用于遍历树、初始化作业或完成作业:

为了演示他们是如何被使用的,请查看下面的动画:

https://i.loli.net/2020/05/14/JwPsxBzA6rq72Fm.gif

这个演示中使用了上面那些函数的简化实现来说明问题。每一个函数接收一个fiber节点并处理它,随着React向下,你可以看到处正在激活的fiber节点如何改变。从动画中你可以清晰的了解到算法是如何从树的一支到另一支。在回溯到父节点之前,将率先处理掉子节点的作业。

注意:垂直连接表示同级兄弟,拐弯节点是子级,上面的例子中,b1节点没有子节点,b2只有一个子节点c1

视频版在这里(好像打不太开)。概念上,你可以认为“开始(begin)”就是“步入(stepping into)”组件,“完成(complete)”就是“步出(stepping out)”组件。作者的例子有些厉害,下面也会介绍。

从前两个函数performUnitOfWorkbeginWork开始:

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的名字来表示(denote)作业执行。beginWork函数总是返回一个指针,指向下一个要在循环中被处理的子节点,或者返回null。

如果还有下一个要处理的子节点(也就是beginWork函数返回不是null),这个节点会被赋值给nextUnitOfWork变量(workLoop函数中循环的依据)。但是如果没有下一个节点了,React知道到了分支的重点,然后就可以complete当前节点。一旦节点被complete,就会对兄弟节点执行作业,之后再回溯到父节点处。这些是在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会退出函数并交出兄弟节点的引用。由workLoop-》performUnitOfWork-》completeUnitOfWork这个顺序会发现,这个兄弟节点的引用将被赋值到nextUnitOfWork变量上,React会开始执行以这个兄弟节点为分支顶的一系列作业。此时,React只是完成了之前兄弟节点的作业,这一点非常重要。父节点对应的作业并没有被completed,因为还没有到回溯的时候。只有当子节点下面的所有分支都completed,才能complete父节点的作业并回溯

workLoopcompleteUnitOfWork主要都是用来迭代,主要活动还是发生在beginWorkcompleteWork函数中。系列后续文章,会具体讨论ClickCounter组件和span节点身上,React步入(steps into)beginWork函数和completeWork函数具体做了什么。

Commit phase 提交阶段

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

当React进入这个阶段,有两棵树和一个effects列表。第一棵树代表了当前渲染在视图上的状态(state)。另一个棵备用(alternate)树在render阶段构建出来。在源码中有两个术语finishedWork和workInProgress代表需要被展示在视图上的状态。这棵备用树的连接方式和current树类似,也是通过child和sibling指针。

然后再来说effects列表,这是一个finishedWork树节点集合的子集(subset),用nextEffect指针连成线性结构。记住,effect列表是跑完render阶段后得到的结果。渲染就是为了确定哪些节点要插入,要更新或者要删除,确定哪些组件有生命周期方法要调用,就用effect list的形式表达。commit阶段要迭代的节点集合,正是effect list

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

commit阶段主要执行的函数是commitRoot。基本上执行了以下动作:

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

在调用了更新前置方法getSnapshotBeforeUpdate之后,React用一棵树提交所有的副作用。分成两个阶段(passes),第一阶段执行所有的DOM(host宿主级别)插入、更新、删除和ref卸载。之后React将finishedWork树赋值给FiberRoot,标记(marking) workInProgress树为current树。

原文:This is done after the first pass of the commit phase, so that the previous tree is still current during componentWillUnmount, but before the second pass, so that the finished work is current during componentDidMount/Update

(我的理解是新旧树交替的这一过程,是在commit阶段的第一阶段后、第二阶段前完成,所以在componentWillUnmount钩子中能访问到的旧树应该还是当前可见的DOM对应的树;在componentDidMount/Update周期当前可用的就已经是处理过的结果)

在第二阶段React调用所有其他的生命周期和ref回调。这些方法以独立开的过程执行,因此整个树中位置调整、数据更新和删除操作都已经被调用(invoked)。

关键的执行步骤描述如下:

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

每一个子函数都实现了一个循环,遍历effect列表并检查effect的类型。这些函数偏离过程中会发现与执行目的相关的effect(函数名代表了归类处理effect)并应用(applies)。

Pre-mutation lifecycle methods 变异前生命周期

下面是一段示例代码,演示了如何迭代effects树和检查有Snapshot effect的节点:

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 DOM更新

React在commitAllHostEffects函数中执行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);
            ...
        }
    }
}

有趣的是React调用componentWillUnmount生命周期是删除操作的一部分,具体是在commitDeletion函数中执行的。

Post-mutation lifecycle methods 变异后生命周期

React在commitAllLifecycles函数中调用其他生命周期,比如componentDidUpdatecomponentDidMount

(原文到这里就省略号要进入系列文章的下一篇了,下一篇文章是“In-depth explanation of state and props update in React”—“深入解释React中state和props更新”)

原文作者:Maxim Koretskyi