本文为意译和整理,若有误导,请放弃阅读。 原文javascript
本文将会带你深刻学习React的新架构-Fiber和新的reconciliation 算法的两个阶段。咱们将会深刻探讨一下React更新state和props,处理子组件的具体细节。html
React是一个用于构建用户界面的javascript类库。它的核心机制是对组件状态进行变动检测,而后把(存在内存中的)已更新的状态投射到用户界面上。在React中,这个核心机制被称之为reconciliation。咱们调用setState
方法,而后React负责去检测state或者props是否已经发生变动,最后把组件从新渲染在屏幕上。java
React文档为这个机制给出了一个很高质量的阐述。react element在这里的角色,生命周期函数,render
方法和应用到子组件身上的diffing算法等方面的阐述包罗万象。从组件的render
方法返回的不可变的react element树被大众称为“virtual DOM”。在React发展的早期,这个概念有助于React团队向人们去解释React的运行原理,可是到了后来,人们发现这个概念过于模糊,容易让人产生歧义。故,在React文档中再也不使用过这个概念了。本文中,我坚持把它叫回“react element tree”(译者注:react element tree从另一个角度来看,它也是一个react element)。node
除了这个react element tree以外,React在内部也维持着一颗叫作“internal instance tree”(这个instance又对应着component实例或者DOM对象)。这颗树是用于保存整个应用的状态的。从React的v16.0.0开始,React对这个“internal instance tree”做了一个新的实现。而用于管理这颗树的算法就是如雷贯耳的Fiber。若是你想要了解Fiber架构所带来的好处,请戳这里:在Fiber架构中,React为何使用和如何使用单链表。react
这是【深刻xxx】系列中的第一篇文章,它志帮助你去了解React的内部架构。在本文中,我将会深刻讲解一些跟算法相关的,重要的概念和数据结构。一旦咱们掌握了这些概念和数据结构,咱们就能够探索整个算法和一些遍历,处理fiber node tree过程当中所用到的主要函数。在这个系列中的下一篇文章中,我将会阐述React是如何应用这个算法去完成界面的的初始渲染和处理state,props的更新。后续,咱们将会继续探索scheduler的实现细节,child reconciliation 的处理流程和构建effect list的机制。git
我会向你输出一些真正高级的知识?是的。我鼓励你去阅读这系列的文章,去了解React Concurrent特性的背后的魔法。我坚信逆向工程(reverse-engineering)的好处,因此,我会给出不少可以跳转到Reactv16.6.0源码的连接。github
整篇文章下来,要接受的东西确实太多了。因此,在你不能立刻理解一个东西的时候,千万不要焦虑。你须要给点耐心,花点时间去理解它,由于这都是十分值得的。注意,你不须要掌握任何React在应用实践方面的知识。这篇文章主要是讲React的内部运行原理。算法
我将会用一个简单的应用示例贯穿整个系列。这个示例是这样的:咱们有一个button,经过点击这个button,咱们能够增长界面上一个数字的值,以下图:vim
下面是它的实现代码:bash
lass 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>
]
}
}
复制代码
你能够在这里玩玩它。正如你看到的代码那样,ClickCounter
是一个简单的组件。这个组件有两个子元素:button
和span
。它们都是从ClickCounter
组件的render方法中返回的。当你点击界面上的button的时候,内部的事件处理器就会更新组件的状态。从而最终致使span元素更新它的文本内容。
React在reconciliation过程当中会执行各类各样的activity。好比说,在本示例中,如下就是React在首次渲染和在状态更新后会执行的主要操做:
ClickCounter
组件的state对象的count属性;ClickCounter
最新的children和它们的props;除此以外,在reconciliation期间,React还有不少别的activity要执行。好比说,调用生命周期函数啊,更新refs啊等等。全部的这些activity在Fiber架构中都被称之为“work”。不一样类型的react element(react element的类型靠其对象的“type”字段来指示)通常有着不一样类型的work。比方说,对于class component而言,React须要建立它的实例对象。而对于functional component而言,React不须要这么作。正如你所知道的那样,在React中,咱们有着各类类型的react element。好比说,咱们有class component,functional component, host component和portal等等。react element的类型是由咱们调用createElement函数时所传递进入的第一个参数所决定的。而createElement函数就是组件render方法用于建立react element的那个函数。
在咱们开始探索各类各类的work和fiber架构的主要算法以前,让咱们先来熟悉熟悉React内部所用到的数据结构。
React中的每个组件都有一个相应的UI representation。它们就是从组件的render函数返回的东西。咱们姑且称之为“view”或者“template”。下面就是咱们示例ClickCounter
组件的“template”:
<button key="1" onClick={this.onClick}>Update counter</button>
<span key="2">{this.state.count}</span>
复制代码
一旦一个template被JSX compiler编译事后,咱们将会获得大量的react element。
更严谨点说,template被JSX compiler编译事后是先获得一个被wrap到render方法里面函数调用。只有render方法被调用了,咱们才能得获得的element。
而这些react element才是组件render方法所返回的真正的东西。本质上来讲,render方法所返回的东西既不是HTML标签,也不是“template”,而是一个【返回react element的】函数调用。“template”,“HTML标签”或者更严格得说“JSX”只是外在模样,咱们根本不须要用它们来表示。下面是用纯JavaScript来重写的ClickCounter
组件:
class ClickCounter {
...
render() {
return [
React.createElement(
'button',
{
key: '1',
onClick: this.onClick
},
'Update counter'
),
React.createElement(
'span',
{
key: '2'
},
this.state.count
)
]
}
}
复制代码
ClickCounter
组件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为这些js对象都添加上了一个【用于惟一标识它们是react element的】属性:$$typeof。除此以外,咱们还有用于描述react element的属性:type
,key
和props
。这些属性值都是来自于你调用React.createElement函数时传入的参数。值得关注的是,React是如何表示span和button节点的文本类型的子节点的,click事件处理器是如何成为props的一部分的。固然,react element身上还有一些其余的属性,好比“ref”字段。不过,它们不在本文的讨论范围内。
ClickCounter
组件所对应的react element没有任何的props和key:
{
$$typeof: Symbol(react.element),
key: null,
props: {},
ref: null,
type: ClickCounter
}
复制代码
在reconciliation(发音:【ˌrekənsɪliˈeɪʃn】)期间,组件render方法所返回的react element身上的数据都会被合并到对应的fiber node身上,这些fiber node也所以组成了一颗与react element tree相对应的fiber node tree。 须要紧紧记住的是,每个react element都会有一个与之对应的fiber node。跟react elment不同的是,fiber node不须要在每一次界面更新的时候都从新建立一遍(译者注:react element偏偏相反。每一次都会被从新建立一遍)。fiber node是一种用于保存组件状态和DOM对象的可变数据结构。
咱们先前提到过,针对不一样类型的react element,React须要执行不一样的activity。在咱们这个示例中,对于ClickCounter
而言,这个activity就是调用它的生命周期方法和render方法。而对于span这个host component而言,这个activity就是执行DOM操做。每一个react element都会被转换成一个相应类型的fiber node。不一样类型的work标志着不一样类型的fiber node。
你能够把fiber node看作一种普通的数据结构。只不过,这种数据结构表明的是某种须要去完成的work。也能够换句话说,一个fiber node表示一个work单元。Fiber架构也同时提供了一种便利的方式去追踪,调度,暂停和中断work。
在createFiberFromTypeAndProps函数中,React利用了来自于react element身上的数据完成了对从react element到fiber node的首次转换。在随后的更新中,对于一些依旧存在的react element而言,React不会从新建立而是复用以前的fiber node。React仅仅在必要的时候,利用其对应的react element身上的数据去更新fiber node身上的相关属性。同时,React也须要根据key
属性去调整fiber node在树上的层级或者当render方法再也不返回该fiber node所对应的react element的时候把这个fiber node删除掉。
你能够在 ChildReconciler 函数中看到全部的activity和用于处理已存在的fiber node的相关函数。
在没引入fiber架构以前,咱们已经存在一颗叫react element tree的东西。在引入fiber架构后,由于React会为每个react element去建立一个与之相对应的fiber node。因此,咱们也就有了一颗与react element tree相对应的fiber node tree。在咱们这个 示例中,这颗fiber node tree长这样的:
正如你说见的那样,全部的fiber node经过child
,sibling
,return
链成了一个linked list。若是你想知道为何这种设计是行得通的,你能够阅读个人另一篇文章 The how and why on React’s usage of linked list in Fiber 。
在application的首次渲染以后,React会生成一整颗的fiber node tree用于保存【那些已经用于渲染用户界面的】状态(也就是react组件的state)。这颗树通常被称之为current tree
。当React进入更新流程后,它又构建了另一颗叫作workInProgress tree
的节点树。这颗树保存着那些即将会被flush到用户界面的应用状态。
全部在workInProgress tree
上的fiber node的work都会被执行完成。当React遍历current tree
的时候,它会为这颗树上的每个fiber node建立一个称之为alternate fiber node
的节点。正是这种节点组成了workInProgress tree
(译者注:也就是说,workInProgress tree
就是alternate fiber node tree
)。React经过利用render方法所返回的react element身上的数据来建立了alternate fiber node
。一旦全部的更新请求(调用setState一次能够视为一次更新请求)都被处理完毕,全部的work也执行完毕的话,那么React就产出了一颗用于将全部变动flush到用户界面的alternate fiber node tree
。在将这颗alternate fiber node tree
映射到用户界面后,它就变成了current tree
。
React的核心准则之一是:一致性(consistency)。React老是一口气地完成DOM更新。这就意味着它不会向用户展现更新到一半的用户界面。workInProgress tree
做为一个“draft”而被用于React的内部,用户是看不到的。因此,React可以先处理完全部的组件,最后,才把须要变动的东西flush到用户界面上。
在React的源码中,你会看到不少的函数实现都是从current tree
和workInProgress tree
同时读取它们的fiber node。下面,就是一个这样的函数的签名:
function updateHostComponent(current, workInProgress, renderExpirationTime) {...}
复制代码
current tree
和workInProgress tree
上的对等的fiber node都会经过一个alternate字段值来保存着一个【指向其对等fiber node的】引用。(译者注:二者处于一种循环引用的状态,伪代码表示以下:)
const currentNode = {};
const workInProgressNode = {};
currentNode.alternate = workInProgressNode;
workInProgressNode.alternate = currentNode;
复制代码
咱们能够把React component理解成一个接受state和props做为输入,最终计算出一个UI representation的函数。全部其余的activity,例如:更改DOM结构,调用组件的生命周期方法等等咱们均可以考虑将其称为“side-effect”,或者简称为“effect”。在React的这篇官方文档中也提到effect的定义:
你以前颇有可能作过相似于data fetching, subscription或者手动更改DOM结构诸如此类的操做。由于这些操做会影响到其余组件,同时它们都不能在rendering期间(译者注:此处的“rendering期间”就是“render阶段”)去完成的。因此,咱们将这些操做称之为“side effect”(或者简称为effect)
你将会看到大部分的state和props的更新都会致使side effect的产生。而又由于应用effect也是一种类型的work。因此,fiber node是一种很好的【,用于去追踪除了update以外的effect的】机制。每个fiber node均可以带有effect。effect是经过fiber node的effectTag
字段值来指示的。
因此能够这么说,一个fiber node被更新流程处理事后,它的effect基本上就定义这个fiber node【须要为对应组件实例所要作的】work。具体点说,对于host component(DOM element)而言,它们的work能够包含“增长DOM元素”,“修改DOM元素”和“删除DOM元素”等。而对于class component而言,它们的work能够包含“update refs”,“调用componentDidMount和componentDidUpdate生命周期函数”。对于其余类型的fiber node而言,还有其余的work存在。
React处理更新流程的速度很是快。为了实现这个性能目标,React应用了几个有趣的技巧,其中之一就是:为了加快迭代的速度,React为那些带有effect的fiber node构建了一个linear list。其中的缘由是由于迭代linear list比迭代一颗tree的速度要快得多。对于那些没带有effect的fiber node,咱们更没有必要花时间去迭代它。
这个linear list的目标把须要进行DOM操做或者有其余effect相关联的fiber node标记出来。跟current tree
和workInProgress tree
中的fiber node是经过child
字段将彼此连接一块儿不一样,这个linear list中的fiber node经过自身的nextEffect
字段来把彼此连接起来的。它是finishedWork tree
的子集。
Dan Abramov 曾经对“effect list”作个一个类比。他把fiber node tree比喻成一棵圣诞树。而圣诞树上把小灯饰链接起来,缠绕着圣诞树的那些电线就是咱们的“effect list”。为了可视化去理解它,让咱们一块儿想象下面这颗fiber node tree中的颜色高亮的节点是带有work的。举个例子,咱们的更新流程将会致使c2
被插入打DOM中,d2
和c1
将会改变自身的attribute,b2
将会调用自身的生命周期方法等等。那么,这颗fiber node tree的effct list将会把这些节点链接到一块,这样在,React在遍历的时候可以作到跳过其余的fiber node:
从上面的图示,咱们能够看到带有effect的fiber node是如何连接到一块的。当React遍历这些节点的时候,React会使用firstEffect
指针来指示出list的第一个元素。因此,上面的图示能够精简为如下的图示:
每个React应用都有一个或者多个DOM元素充当着container的角色。在本示例中,有着id值为“container”的div元素就是这种container。
const domContainer = document.querySelector('#container');
ReactDOM.render(React.createElement(ClickCounter), domContainer);
复制代码
React为每个container都建立了fiber root。你能够经过DOM元素身上保存的引用来访问它:
const fiberRoot = query('#container')._reactRootContainer._internalRoot
复制代码
这个fiber root就是React保存fiber node tree引用的地方。fiber root是经过currnet
的属性值来保存这个引用的:
const hostRootFiberNode = fiberRoot.current
复制代码
fiber node tree以一个特殊类型的fiber node开头。这个fiber node被称之为HostRoot
。它是React内部建立的,被当作是你最顶层组件的父节点。同时,经过HostRoot
的stateNode
字段,咱们能够回溯到FiberRoot
身上来:
fiberRoot.current.stateNode === fiberRoot; // true
复制代码
你能够经过访问最顶层的HostRoot
来探索整一颗fiber node tree。又或者,你能够经过访问一个组件实例的_reactInternalFiber属性来访问某一个单独的fiber node:
const fiberNode = compInstance._reactInternalFiber
复制代码
下面,让咱们一块儿来看看,本示例中ClickCounter
组件的fiber node是长什么样的吧:
{
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
}
复制代码
fiber node这种数据结构身上仍是挺多字段的。我已经在先前的章节中解释过alternate
,effectTag
,nextEffect
这几个字段的用途了。下面,咱们来看看为啥还须要其余字段。
这个字段保存着组件实例,DOM对象等react element type的引用。 通常状况下,咱们能够说这个字段保存着fiber node所关联的local state。
这个字段定义了与当前fiber node相关的function或者class。对于class component而言,该字段值就是这个class的constructor函数。而对于DOM元素而言,该字段值就是这个DOM元素所对应的就是字符串类型的HTML标签名。我常常用这个字段去理解一个fiber node所关联的react element是什么样子的。
这个字段定义了fiber node的类型。在reconciliation算法中,它被用于决定一个fiber node所须要完成的work是什么。早前咱们提到过,work的具体内容是由react element的类型(也就是type字段)来决定。createFiberFromTypeAndProps函数负责将某个类型的react element转换为对应类型的fiber node。具体落实到本示例中,ClickCounter
fiber node的tag值是“1”,这就表明着这个fiber node所对应的react element是ClassComponent类型 。对于span
fier node而言,它的tag值是“5”。这就表明着它所对应的react element是HostComponent类型。
A queue of state updates, callbacks and DOM updates.
已经被用于建立output的fiber node state。若是咱们当前是在处理流程中, 那么fiber node的该字段保存的就是那些已经被渲染到用户界面的状态。
已经被用来在上一次渲染期间建立output的fiber node props。
当前渲染期间,已经被更新的了,等待被应用到child component或者DOM元素身上的fiber node props。fiber node props的更新是经过利用来自于react element中的数据来完成的。
惟一标识一个children列表中的每个item。它被用于帮助React去计算出哪一个item被更改了,哪一个item是新添加过来,哪一个item被移除了。React在这篇General algorithm文档中对它(指的是key字段)更加具体的做用做出了很好的阐述。
你能够在这里找到fiber node完整数据结构的说明。在本文中,我已经跳过了不少的字段了。须要特别提到的是,本文中没有说起的,用于将各个fiber node连接成树状结构的child
,sibling
和return
字段,其实我已经在先前的这篇文章中说明过了。而归属于其余分类的字段,好比,expirationTime
,childExpirationTime
和mode
等字段都是跟Scheduler
相关的。
React执行work的过程分为两个阶段:render阶段和commit阶段。
在render阶段,当用户调用setState()
或者React.render()
方法的时候,React就会对组件实施更新操做,而后计算出整个用户界面中须要更新的地方。若是是组件初次挂载的render阶段,React会为每个【从组件render方法返回的】react element建立与之对应的fiber node。在下一次的render阶段里面,若是某个fiber node所对应的react element还存在的话(译者注:在每一次更新中,都会调用render方法。拿返回的react element跟以前的react element判断,就知道该react element是否还存在),那么这个fiber node将会获得复用和更新。render阶段的最终目的是产出一颗标记好effect(是否带有effect,effect的类型是什么)的fiber node tree。一个fiber node的effec字段是用于描述在接下来的commit阶段,这个fiber node须要完成的work有哪些。在这个commit阶段,React接收一棵标记好effect的fiber node tree做为输入,而后将它应用到其对应的实例上。具体点来讲,就是遍历effect list,根据相应的effect去执行相应的work:DOM更新或者其余结果对用户可见的操做。
须要明白的一点是,render阶段的work的执行能够是异步的。取决于可用的时间,React能够处理一个或者多个fiber node,须要让步给其余事情的时候,React就暂停处理,将手头上已经完成的work暂存起来。而后,从它上次中断的地方继续执行。也不老是如此,有时候React仍是会放弃掉已经完成的work,从头(译者注:这个“头”就是fiber node tree的第一个节点)开始作起。之因此可以暂停执行work是因在render阶段执行的work并不会对用户产生可见的界面效果,好比说,DOM更新就会产生可见的界面效果。于此相反,在接下来的commit阶段老是同步执行的。那是由于这个阶段须要执行的work是会对用户产生可见的界面效果的,因此,React会一口气完成这个流程(指commit阶段)。
“调用生命周期方法”是React须要执行的一种work。其中,一部分的生命周期方法会在render阶段被调用,而另一部分会在commit阶段调用。下面是render阶段会调用的生命周期方法:
正如你所看的那样,一些以“UNSAFE”为前缀的,已经被遗弃的(legacy)生命周期方法(Reactv16.3版本引入此更改)会在render阶段执行。如今,这些方法在React的官方文档中都被称之为“遗弃的生命周期方法”。这些方法将会在某个16.x的版本中被废弃(deprecated)。而它们的孪生方法,没有以“UNSAFE”为前缀的那些生命周期方法将会在17.0中被移除(removed)。你能够在这篇文档中看到这方面变更的介绍和API迁移方面的说明。
你是否是对这个变动背后的缘由感到好奇呢?
好吧,在这里,我给你说道说道。正如咱们前面所说的那样,由于render阶段,React不会产出(produce)effect,好比DOM更新之类的。同时,React可以对组件进行异步的更新(甚至有可能以一种多线程的方式去作)。然而,那些以“UNSAFE”为前缀的生命周期方法在实际生产中常常被误解和误用。开发者常常在这些生命周期方法里面放置一些有(side-)effect的代码。引入fiber架构后,React也为咱们带来了异步渲染的方案。若是开发者继续这么作的话,程序是会出问题的。尽管他们的孪生方法(没有以“UNSAFE”为前缀的那些)最终会被移除掉,可是他们依然可能会在将来的Concurrent Mode
(你也能够选择不开启Concurrent Mode
)特性版本中产生问题。
下面是commit阶段会执行到的生命周期方法:
由于这些方法被执行的阶段是同步执行的,因此,它们能够包含side-effect代码和访问DOM元素。
好吧,讲到这里,咱们已经能具有足够的知识储备去理解【被用于遍历fiber node tree和执行work的】generalized algorithm
。
reconciliation老是从fiber node tree最顶端的HostRoot
节点开始执行。这个开始动做发生在renderRoot函数里面。然而,React会跳过那些已经处理过的fiber node,只会处理那些有带有未完成work的节点。举个例子说,若是你在组件树的深层去调用setState
方法的话,那么React虽然仍是会从顶部的节点开始遍历,可是它会跳到前面全部的父节点,径直奔向那个调用了setState方法的子节点。
全部的fiber node都会在一次的work loop中获得处理。下面是work loop同步执行部分的代码实现:
function workLoop(isYieldy) {
if (!isYieldy) {
while (nextUnitOfWork !== null) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
} else {...}
}
复制代码
在上面的代码中,nextUnitOfWork
保存着一个指向workInProgress tree
树上某个还有未完成work的fibe node的引用。当React在这颗fiber node tree上遍历的时候,它就是使用这个变量来判断是否还有别的fiber node须要处理。当前fiber node一旦被处理后,nextUnitOfWork`变量的值要么是指向下一个fiber node,要么就是null。一旦是null的话,React就会退出work loop,而后准备进入commit阶段。
有四个函数是用于遍历fiber node tree,初始化或者完成work的:
它们是如何被使用的呢?让咱们看看下面React遍历fiber node tree时的动画。为了演示,咱们使用了这些函数的精简版实现。这四个函数都接受一个fiber node做为输入。随着React对这颗树的往下遍历,你能够看到当前active的fiber node(橙色节点表明active的fiber node)在不断地更改。你从这个动画上清晰地看到这个算法是如何从fiber node tree的一个分支切换到另一个分支的。具体点说就是,先处理完全部children的work再回溯到parent节点(译者注:其实就是深度优先遍历):
请注意,上图中,竖线是表明sibling关系,横折竖线表明着children关系。例如:
b1
就没有children,而b1
有一个children叫作c1
这里是一个相关的视频连接。在这个视频里面,你能够暂停和回放,而后仔细瞧瞧,当前的fibe node是谁,当前这个函数的状态是怎样的。理论上说,你能够把“begin”理解为“stepping into”一个组件,而“complete”就是从这个组件中“stepping out”。你也能够玩玩这个demo。在这个demo中,我解释了这几个函数具体都作了什么。这个demo的代码以下:
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;
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) {
// 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 returnFiber. workInProgress = returnFiber; continue; } else { // We've reached the root.
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);
}
复制代码
下面一块儿瞧瞧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 tree
的fiber node做为输入,经过调用beginWork
函数来开始执行work。React将会在beginWork
函数里面开始去完成一个fiber node全部须要被完成的work。为了简化演示,我把所须要完成的work假设为:打印当前fiber node的名字。beginWork 函数要么返回一个指向下一个work loop须要处理的fiber node的引用,要么返回null。
若是有下一个待处理的child fiber node的话,那么这个fiber node就会在workLoop
函数里面将它赋值给变量nextUnitOfWork
。不然的话,React 知道已经触达了当前(fiber node tree)分支的叶子节点了。所以,React能够结束当前fiber node(的work)了。一旦一个fiber node被结束掉,React接着会执行它的sibling节点的work,在完成这个sibling的这个分支后,就会移步到下一个sibling节点.....以此类推。当全部的sibling节点被结束到,React才会回溯到parent节点。。这个过程是发生在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 is no more work in this returnFiber,
// continue the loop to complete the parent.
workInProgress = returnFiber;
continue;
} else {
// We have reached the root.
return null;
}
}
}
function completeWork(workInProgress) {
console.log('work completed for ' + workInProgress.name);
return null;
}
复制代码
正如你所看到的那样,completeUnitOfWork的函数实现主体是一个大while循环。当workInProgress node
没有children的时候,React的执行就会进入这个函数。在完成当前fiber node的work以后,它就紧接着去检查是否还有下一个sibling节点要处理。若是有的话,那么就把下一个sibling的引用返回出去,退出当前函数。所返回的sibling的引用会赋值给nextUnitOfWork
变量,而后React就从这个sibling开始新一轮的遍历。值得强调的一点是,上面所提到的这个节骨眼上,React只是完成了先前sibling节点的work,它并无完成parent节点的work。当一个fiber node本身和其全部子节点分支上的work都被完成了,咱们才说这个fiber node的work完成了,而后才往回追溯。
正如你所看到的那样,performUnitOfWork
和completeUnitOfWork
主要是用于对fiber node tree进行迭代。迭代中具体须要干的事都是靠beginWork
和completeWork
去实现的。在本系列中接下里的文章中,咱们会了解到随着React执行到beginWork
和completeWork
函数,ClickCounter
组件和span
组件到底发生了什么。
这个阶段以completeRoot函数开始。在这个函数里面,React完成了DOM更新和对pre-mutaion和post-mutation生命周期方法的调用。
进入commit阶段后,React须要跟三个数据结构对象打交道:两棵tree,一个list。两颗tree分别是指current tree
和workInProgress tree
(或者称为finishedWork tree
),一个list是指effect list
。current tree
表明着当前已经渲染在用户界面的应用的状态。workInProgress tree
是React在render阶段构建出来的,视为current tree
的alternate。它表明着那些须要被flush到用户界面的应用状态。workInProgress tree
跟current tree
同样,也是经过fiber node的child和sibling字段将本身连接起来的。
effect list是finishedWork tree
的一个子集。它是经过fiber node的nextEffect
字段将本身连接起来的。再次提醒你,effect list是render阶段的产物。render阶段所作的一切都是为了计算出哪一个节点须要被插入,哪一个节点须要被更新,哪一个节点须要被移除,哪一个组件的生命周期方法须要被调用。这就是effect list能告诉咱们的信息。effect list上面的fiber node才真正是commit阶段须要被遍历到的节点。
debug的时候,你能够经过
fiber root
的current
属性值来访问current tree
。你能够经过HostFiber
节点的alternate
属性来访问finishedWork tree
上对应的fiber node。详细能够查看Root of the fiber tree这一小节。
commit阶段的主要功能函数是commitRoot。能够这么说,它作了下面这些事:
snapshot
effect的fiber node,调用它的getSnapshotBeforeUpdate
生命周期方法Deletion
effect的fiber node,调用它的componentWillUnmount
生命周期方法current
指针指向finishedWork tree
(在javascript里来讲,就是引用传递)。Placement
effect的fiber node,调用它的componentDidMount
生命周期方法Update
effect的fiber node,调用它的componentDidUpdate
生命周期方法在调用完pre-mutation方法getSnapshotBeforeUpdate
后,React会将树上全部的effect commit掉。这个操做又分为两步走。第一步是:执行全部的DOM节点的插入,更新,删除和ref的卸载。而后,React会把finishedWork tree
赋值给FiberRoot
节点的current
属性,以此把finishedWork tree
转换为current tree
。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. 第二步:React调用全部的其余生命周期方法和ref callback。由于这些方法都是在一个独立的步骤里面执行的。到这个时候,树上全部的placements,updates和deletionseffect都已经被应用过了。
下面是commitRoot函数中上面提到两个执行步骤:
function commitRoot(root, finishedWork) {
commitBeforeMutationLifecycles()
commitAllHostEffects();
root.current = finishedWork;
commitAllLifeCycles();
}
复制代码
全部的这些子函数都实现了对effect list的迭代。在迭代的过程当中,它们都会检查当前fiber node的effect是不是本函数须要处理类型,若是是,则应用该effect。
如下是一个小示例,里面的代码实现了对effect list的遍历,而且在遍历的过程当中去检查当前的fiber node的effect type是不是Snapshot
:
function commitBeforeMutationLifecycles() {
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
if (effectTag & Snapshot) {
const current = nextEffect.alternate;
commitBeforeMutationLifeCycles(current, nextEffect);
}
nextEffect = nextEffect.nextEffect;
}
}
复制代码
对一个class component而言,“应用这个snapshot
”effect“等同于“调用getSnapshotBeforeUpdate
生命周期方法”;
React会在commitAllHostEffects 函数里面完成全部的DOM操做。这个函数罗列DOM操做的类型和具体的操做:
unction 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
方法的调用划分到deletion
这一类别中,最终在commitDeletion
函数中调用了它。
剩下还有componentDidUpdate
和componentDidMount
这两个生命周期方法。它们会在commitAllLifecycles 里面被调用。
最终的最终,咱们终于讲完了。若是你想说出你对这篇文章的看法又或者想问问题,欢迎评论评论。同时也欢迎查阅个人下一篇文章:深刻react的state和props更新。在我打算写完的这一【深刻xxx】系列中,我手头上还有不少正在写的文章。这些文章囊括了“scheduler”,“children reconciliation”和“effects list是如何构建起来的”等方面的主题。同时,我也打算基于本文所讲内容发布一个讲解何如debug的视频,欢迎翘首以盼。