公告 / Notice

  • 界面已全面升级(11/06)

    CSS & JS分别用SASS和Webpack编译,对资源进行了模块化处理;缓存规则使页面运行更快!

  • 移动端完成(07/22)

    做得比较随意,UI非常简便

  • 正式上线啦!(07/19)

    星博客V2.0已经正式上线,欢迎大家登录浏览!

  • 星博客V2.0启动(07/15)

    由于不满意1.0版本的UI风格,星博客V2.0今日进入开发阶段

深入Fiber:React16新Reconciliation算法全面深度剖析
React

React是一款用于构建用户界面的库。React是如何跟踪组件内的状态变化并将变化投射到页面上,其核心阐明了这种机制。我们知道在React中,这种机制被称为reconciliation。我们调用setState方法,React检测组件的状态或属性是否改变,并重新渲染组件到界面上。

对此机制,React的官方文档对其关键要素给出了高度概括性的描述:React元素的作用、生命周期函数和render方法、以及用于判断组件变化的DIFF算法。经过React组件的render方法返回的元素具有不可更变性,其形成一个树结构,通常被称为“Virtual DOM”。这个名词早期很能帮助使用者解释一些东西,但也存在一些叫人困惑的地方,如今React官网不再使用之。在本文中,我将一贯称之为React元素树

除了React元素树,React还会有一棵由内部实例(组件、DOM节点等)构成的树,以保持应用的状态。从最新的版本16开始,React推出了一种新的管理内部实例树的算法,称之为Fiber。要了解Fiber架构带来的优点,请查看另一篇文章《React为何以及如何将Fiber设计为链表结构》(翻译中...)。

写出这篇文章,我花费了较之更长的时间。倘如没有大神Dan Abramov的帮助,这篇文章的深度将大打折扣!

本篇文章是介绍React内部架构的系列的第一篇。在此,我欲对那些重要的概念、涉及算法的数据结构作一个深度而且全面的介绍。一旦我们有了一定的知识背景,我们将去研究一下用于遍历、处理Fiber树的算法和主要的函数。系列的次篇,我将演示React是如何使用此算法来执行初始渲染和后续的状态、属性更新。我们将深入到Scheduler(计划)、子节点reconciliation处理、构建effect链表之原理的细节当中。

我将在此告诉你一些较为高级的知识。希望你能耐心地读完,如此方能识透Concurrent React内部运作背后的机关。如果你想要参与到React开源项目的开发、维护工作,这一系列的文章也将有所帮助。我是reverse-engineering的坚定信徒,因此,文中出现的源链接多来自最新版本16.6.0。

知识量庞大,需要你花费大量的精力,如果有些地方不是很明白,请不要有压力。多花些时间去掌握,那很是值得的。请注意,仅仅是使用React来开发项目的话,大可不必去了解这些。这篇文章要介绍的是React内部工作原理。

预备

我准备了一个极其简单的应用,它将贯穿这整个系列。页面上有一个button,每次点击将导致其右边的数字加一:

 

Application Increment

以下代码为其实现:

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元素内的文本。

在整个reconciliation期间,React会存在大量的操作。比如,本示例中,在组件首次渲染以及状态更新之后,React会存在以下几个操作:

  1. 更新组件 ClickCounter 状态中的 count 属性
  2. 获取并比较 ClickCounter 的子节点以及它们的属性
  3. 更新 span 元素的属性

还有其它一些操作,例如调用生命周期方法(Lifecycle methods)或更新元素引用(Refs)。所有这些操作在Fiber架构中被称为Work。不同种类的元素(React元素)对应不同种类的Work。比如,对于 Class 组件,React需要创建一个组件的实例,这不会在 Functional 组件上执行。如你所知,我们有很多种(React)元素类型,如 class 和functional 组件、host 组件(DOM 节点)、portals 等等。React元素的类型(type)由函数createElement的第一个参数决定。这个函数一般用于render方法中以生成一个element。

在正式开始介绍这些操作和fiber算法的要义之前,让我们首先来熟悉下React内部定义的数据结构。

从React元素到Fiber节点

每一个组件都有一个由render方法返回的UI呈现,我们可以称之以View或者Template。ClickCounter组件的Template如下:

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

React Elements(React元素)

一旦JSX编译器编译完一个Template,你会得到一组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的参数。特别注意一下,对于 buttonspan 元素的 textContent 属性,React是如何表达的。对于 button 元素的click事件处理函数,React又是如何表达的。还有其它的一些属性,如 ref,这超出了本文的范畴。

ClickCounter 组件生成的元素没有任何props和key:

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

Fiber nodes(fiber节点)

在reconciliation期间,由组件的render方法所返回的数据会被合并到fiber树中。每一个React元素都有一个对应的fiber节点。不像React元素,fiber节点不会在每次render的时候重新创建,它们是可变更的数据结构,fiber以此保存着组件的状态和对DOM节点的引用。

我们之前说过,对于不同类型的元素,React需要做不同的操作。在我们的例子中,对于class组件 ClickCounter ,React会调用生命周期方法、render方法;而对于span元素,其被称为 host 组件(DOM节点),React会执行必要的DOM变更。因此,每一个React元素,根据其类型转为对应类型的Fiber节点,Fiber节点的类型告诉React要对此类节点做些什么。

你可以这么考虑Fiber,它以特定的数据结构表达了要做的一些事,或换句话,一个工作单元。Fiber架构为任务跟踪、任务计划、任务暂停、任务丢弃的实现提供了便利。

 

当一个React元素首次被转化为Fiber节点的时候,React使用element数据作为参数,来调用函数 createFiberFromTypeAndProps。随后的更新,React会复用这些Fiber节点,根据新生成的element数据,仅仅更新那些需要改变的属性。React可能会根据元素的key属性,在Fiber结构中移动或移除节点。

去看看 ChildReconciler 函数,了解一下React对于构建起来的Fiber节点,所做的全部操作及其对应的函数实现。

由于React会为每一个元素创建Fiber节点,这样当我们有了一个元素树之后,也就有了一个Fiber节点树。拿我们这个例子来说,其结构类似:

 

Fiber节点结构
图:React Fiber节点结构

所有Fiber节点链接起来,形成了一个链表结构,这得益于节点上的三个属性:childsiblingreturn。至于为什么要这样设计,请参考文章《React为何以及如何将Fiber设计为链表结构》。

Current and work in progress trees (当前树和工作树)

首次渲染之后,React持有一个fiber节点树,它反映了用以渲染了UI的当前状态。这个树(tree)被引用为 current。应用状态变更的时候,React会构建一个所谓的 workInProgress 树,它反映了一个未来的、用以更新UI的状态。

全部的操作都发生在 workInProgress 树上。React遍历 current 树时候,对于每个fiber节点,创建一个alternate节点,这些alternate节点组建起了 workInProgress 树。alternate节点是根据render方法返回的数据创建出来的。当状态更新得到处理、并其相关的全部Work已完成,React拥有了一个alternate树,它将被绘制到界面上。当这个alternate树(也就是workInProgress树)被绘制到界面上,它就变为 current 树。

React的核心理念之一就是统一性(consistency)。React总是一气呵成地完成全部的DOM更新,而非部分地进行。workInProgress 树充当草稿的作用,对于用户不可见,因此React可以先处理全部的组件更新,然后来将变化反映到UI上。

在React源码中,你会发现。存在很多的函数,它们同时接收了 currentworkInProgress 树上的 fiber 节点。下面是个例子:

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

每个fiber节点持有来自其它树(current、workInProgress)的对偶节点,它被存储在 alternate 字段中。一个来自 current 树的节点的 alternate 属性指向来自 workInProgress 树的一个与之对偶的节点,相反也是如此。

Side-effects

我们可以这样看待React组件,它是一个函数,使用状态(state)、属性(props)来计算UI呈现。其它的操作,例如更新DOM、调用生命周期函数,应该被称作side-effect,或者简单些,叫做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.

你可以看到,多数状态、属性的变更将导致side-effect。由于应用effect(使effect生效、用户可体验)是一种类型的工作(Work),fiber节点的运行机制可以方便地对于effect,包括状态更新(updates),进行跟踪。每一个fiber节点会有effects与之相关联,它们的值被编码并存放在 effectTag 字段中。

因此在Fiber中,组件实例的更新得以处理之后,effects对需要做的工作(Work)做了基本定义。对于host组件(DOM元素)这些工作(Work)包括添加、更新、移除元素。对于class组件,React可能会更新refs、调用 componentDidMount、componentDidUpdate等生命周期函数。对于其它类型的fiber节点,对应不同的effects。

Effects list(Effect 链表)

React处理更新非常之快,为了达到这样的性能,它使用了几个有趣的技术。其中之一,就是将fiber节点构建成一个链表结构,节点携带effects,这样可快速迭代。线性迭代比树形迭代要快得多,并且React不会在没有side-effects的节点上花费功夫。

Effect链表存在的目的是,标记fiber,告诉React,有DOM更新或其它类型的Work要做。它是 finishedWork 树的一个子集,以 nextEffect 属性联系起来(current和workInProgress树用的是child属性)。

Dan Abramov 给出了一个effect链表的结构图。他喜欢称之为结着圣诞灯的圣诞树,这些灯将包含有effect的节点联系在一起。为使其可视化,让我们来想象,以下这个棵fiber节点树,高亮的节点表示其有effect,有等待处理的Work。举个例子,c2节点将被插入到DOM中,d2和c1需要修改属性,b2即将触发周期函数。effect链表将如此链接起来,以便React能够跳过其它节点:

 

图:Effects list

你能看到,这些包含effects的节点是如何联系到一起的。遍历节点的时候,React使用 firstEffect 指针来决定从链表的何处开始。因此,上图可以表现为线性形式如下:

 

图:Effect linear list

可以看到,React以从子到父的顺序应用effects。

Root of the fiber tree(fiber树的根)

每一个React应用都有至少一个DOM元素充当容器。在我们的这个例子中,它是一个ID为container的div元素。

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

React会为每一个容器创建一个fiber root对象,你可以用根DOM元素的以下属性访问到这个对象:

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

React以此持有整个fiber树,它存储在 current 属性中:

const hostRootFiberNode = fiberRoot.current

fiber tree 始于一个特殊的fiber节点类型,即 HostRoot。它由内部创建,作为根级组件所属fiber节点的父节点。它有一个stateNode属性,用以指向fiberRoot对象:

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

由此,你可通过fiber root拿到 HostRoot 根节点,来遍历整个fiber树。或者,你也可以拿到任意一个React组件实例对应的fiber节点,以此方式:

compInstance._reactInternalFiber

fiber node structure(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 node:

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

还有许多其它的字段,不在此列举。我在上一部分中解释过字段 alternateeffectTagnextEffect 的作用,现在让我们来看看为什么需要其它几个。

 

stateNode

对当前fiber node所关联的组件实例、DOM节点或者其它React元素的引用。一句话,我们可以说这个字段存储了fiber node的本地状态。

type

定义了fiber node相关联的function或class。对于class组件,它指向其构造函数;对于DOM元素,它是HTML标签。我常常使用这个字段来了解一个fiber node所关联的是什么样的元素。

tag

定义fiber node自己的类型。它告诉reconciliation算法,对于一个fiber node,该执行什么样的Work。前面提到,Work因React元素的类型不同而不同。函数 createFiberFromAndProps 将各类型的React元素映射为对应类型的fiber node。在我们的应用中,对于 ClickCounter 组件,属性 tag 的值为 1,它表示 ClassComponent;对于 span 元素,tag 的值为 5,它表示 HostComponent

updateQueue

一个承载状态更新、回调、DOM变更等数据的队列。

memoizedQueue

用来产生了输出(子组件)的状态数据,正在处理更新的时候,它反映了当前已经用以渲染到UI上的状态。

memoizedProps

在前一轮的渲染中,已经用来产生了输出(子组件)的属性数据。

pendingProps

由React元素的新数据,更新之后的属性数据,它将要被用到子组件或DOM元素上。

key

用于标识一组子节点中的每个节点的唯一性。它帮助React辨识那些项发生了改变,需要从子节点组中添加或移除。React文档对此有描述

 

你可以去GitHub上查看fiber node的完整结构,上面的解释中,我忽略了很多的字段。尤其是,我跳过了 childsiblingreturn 这三个用来构建树结构的字段,关于这个,我在前一篇文章有解释。至于 expirationTimechildExpirationTimemode 字段,它们属于 Scheduler 范畴。

General algorithm(算法梗概)

React执行工作(Work)分为两个阶段:rendercommit

在首次 render 阶段,通过调用 setStateReact.render 计算出来的UI更新的数据表达被列入计划,React按照计划将这些更新的数据表达应用到组件上。如果这是首次render,React将为每一个组件,根据其render方法的返回值,创建一个fiber node。在后续的更新,对于尚存在的组件,其对应的fiber node会被再次使用并得以更新。render 阶段的结果就是一个标记着side-effects的fiber树。这些effect描述了将要在commit阶段完成的工作(Work)。在commit阶段,React获得了一个标记着effects的fiber树,并将effects应用到组件实例上。React遍历整个effects链,执行DOM更新以及其它用户可见的变化。

很重要的一点,你需要知道,首次 render 阶段的Work可以异步地去完成。React可以根据可用的时间,来处理一个或多个fiber node,然后停止,将做毕的Work暂存起来,转而去响应一些事件。接着,React将从暂停的地方开始继续执行Work,有些时候,它可能会忽略之前完成的Work,从头来。这个阶段发生的一些暂停不会导致用户可见的变化,比如DOM更新。相反,接下来的 commit 阶段却总是同步的。这是因为这个阶段要执行的工作会导致用户界面的变化(用户可见)。这就是为什么React需要一次性地搞定它。

调用生命周期函数是React要执行的Work中的一种。其中有一些方法在 render 阶段调用,另外一些在 commit 阶段调用。以下列出在首次 render 阶段需要调用的生命周期函数:

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

你一定注意到了,其中一些遗留的周期函数从版本16.3开始,被标记了 UNSAFE。在官方文档中,它们现在被称为历史遗留周期函数。它们将在版本 16.x 发布的时候不建议使用,其对应的函数(无 UNSAFE 前缀),将在版本 17.0 的时候被移除。关于这些变化以及迁移建议,你可以去官方文档了解更多。

你一定对 UNSAFE 感到好奇,对吧?

好的,我们方才了解到 render 阶段不会产生如DOM更新的side-effect,React可以异步地来处理组件的更新(理论上,甚至可以多线程处理)。然而,被标记为 UNSAFE 的那些周期函数经常被误解并且无意误用。开发者会想着把产生 side-effects的代码放到这些函数中,这样可能会导致新的异步渲染(render)。尽管只将移除对应的无 UNSAFE 前缀的周期函数,这些Unsafe的周期函数,在即将上场的 Concurrent 模式下,仍然会导致一些问题。

commit 阶段,以下方法会被调用:

  • getSnapshotBeforeUpdate
  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount

由于这些方法是同步执行,它们可能包含产生side-effect、操作DOM的代码。

好了,我们现在有了一定的基础,是时候了解下遍历树并执行Work的算法的概要。让我们沉浸下去吧!

Render phase

我们的reconciliation算法总是从最顶级的fiber节点开始,使用的是 renderRoot 函数。当然了,React会跳过那些已经处理过的或无需处理的节点,直到找到一个携有待处理Work的节点。举例来说,如果你在组件树的深层节点调用了 setState ,React将从树根开始遍历,但是跳过多层父组件之后,很快便能找到这个组件。

Main steps of work loop

所有的fiber节点都在一个叫做  work loop  循环中得以处理。以下是这个循环同步部分的实现:

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

以上代码中,nextUnitOfWork 持有来自 workInProgress 树的一个fiber节点,这个节点有待处理的Work。在React遍历Fiber树的时候,它使用这个变量来判断是否还有其它包含待处理Work的fiber节点。当前节点的Work处理完毕,nextUnitOfWork 将指向下一个节点或者 null。一旦为 null,React将退出 Work loop,并准备进入 commit 阶段以提交变化。

主要有4个函数,来遍历Fiber树,初始化或者结束一个Fiber上的Work:

  • performUnitOfWork
  • beginWork
  • completeUnitOfWork
  • completeWork

为了演示它们是如何运行的,看下下面的遍历fiber树的动画。为演示起见,我对这些函数做了精简化实现。每一个函数接收一个fiber节点,随React深入到树的各个节点,可以看到当前正在被处理的节点(注意颜色)如何变化。通过视频,你可以清楚的看到我们的算法是如何从一个分支到另一个分支。它首先完成子节点上的Work,然后移至父节点。

图:Fiber节点遍历,蓝色表示初始状态,橙色表示正在处理状态,绿色表示处理完毕

注意,纵向链接表示兄弟节点,横向链接表示子节点,比如 b1 没有子节点,而 b2 有一个子节点 c1

提供一下视频链接,借此,你可以暂停播放来观察当前节点的状态以及4个函数调用情况。从概念上来说,你可以将“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;
}

函数 performUnitOfWorkworkInProgress 树接收一个fiber node,通过调用 beginWork 函数开始处理节点Work。全部的Work均在函数 beginWork 中处理。见于演示之用,我们只是简单地console.log出fiber节点的name属性,以表示Work已经被处理掉。beginWork 总是返回下一个child的引用或者 null

如果存在下一个child,它将在workLoop函数中被赋值给变量 nextUnitOfWork 。如果没有下一个child,React知道已经到了当前分支终点,因此可以结束当前节点。一旦节点结束处理,React需要处理兄弟节点上的Work,之后返回到父节点。这些在函数 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 循环。React在 workInProgress 没有child的时候进入这个函数。在结束当前节点Work之后,检查是否有兄弟节点。如果有,函数返回对这个兄弟节点的引用。React将这个节点引用赋值给变量 nextUnitOfWork,并开始处理以这个兄弟节点开端的分支。一定要明白,这个时候,React已经结束了之前的兄弟节点上的Work,父节点上的Work还没有结束呢。只有所有自子节点开端的分支上的Work得以结束,React才会来结束父节点的Work,并原路返回。

从实现你可以看到,performUnitOfWork 和 completeUnitOfWork 主要用于迭代之目的,而主要的、实质的工作在 beginWork 和 completeWork 两个函数中完成。在下一篇文章中,我们将了解到对于 ClickCounter 组件和 span 节点,beginWorkcompleteWork 都做了些什么。

Commit phase

这个阶段始于函数completeRoot。此时,React开始更新DOM并且调用更新前、后的生命周期方法。

当React进入这个阶段,存在 2 个树和一个 effect链表。第一个是已经呈现到UI的树,然后还有一个alternate树,它创建于 render 阶段,在源码中被叫做 finishedWork 或者 workInProgress, 并且将要呈现到UI。和current树类似,alternate树也通过 childsiblingreturn 指针构成。

effect链表是 finishedWork 树的一个子集,它们仰仗 nextEffect 指针形成。记住,effect链表是 render 阶段结束后的产物。 render阶段的全部要义就是明确哪些DOM节点要插入、更新、删除,哪些组件需要触发其生命周期方法,这些最后都由effect list告诉我们。实际上,commit 阶段要遍历的仅是这个链表!

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

commit 期间需要调用的主要函数是 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 的时候,current 树依然是之前的(有不明确之处)。在第二步,React调用全部其它的周期方法以及ref callbacks。这些方法在在单独的一步里来执行,因此可以确保之前在整个树上所发生的DOM变更都已经得到执行。

commitRoot函数的实现大概如下:

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

每一个在其中调用子函数都会去迭代effect list,检查effect的类型(type)。当发现effect适于在当前函数中使用,则使用(applies)之。

Pre-mutation lifecycle methods

以下是一个示例,遍历整个effect链表并检查存在标记有 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组件,这个effect意味着要调用 getSnapshotBeforeUpdate 周期方法。

DOM updates

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

函数 commitAllLifecycles 是React用来调用剩余周期方法 componentDidUpdatecomponentDidMount 的。

结束

本文是翻译而来的,作者是Max Koretskyi,在此表示感谢。后续还有他在Medium上发表的若干文章的翻译,敬请期待。

本人翻译能力有限,有错误、牵强、不当之处,请留言指出,谢谢。