原文引用:in-depth overview of the new reconciliation algorithm in React
做者:Max Koretskyijavascript
React
是一个用于构建用户界面的 JavaScript
库。它的核心原理是追踪组件中的状态的改变,而后将这些被更新的状态自动刷新到屏幕上。在 React
中有一个过程被称之为 协调 reconciliation
。也就是当咱们调用 setState
方法,或是框架检测到 state
or props
变化后,便开始从新开始计算比对组件的先后状态,并渲染与之对应的改变的 UI。
React 官方文档提供了对其原理的高阶概述:React Element
,生命周期方法,render
方法,组件孩子节点的 diff
算法的应用等 在 React
中所扮演的角色。其中 render 方法所返回的 React elements Tree
就是咱们经常提到的 虚拟DOM virtual DOM
。这个术语有助于前期向人们解释 React
,可是它也会形成一些困惑,由于任何 React
的官方文档中都没出现过它。因此在这篇文章中,我将坚决称它为 React elements
树。
除之外,React 还有另一棵内部实例树(组件实例,或 DOM 节点等),它被用来保存 state 状态。从 React16 开始 React 推出了新的内部实例树的实现 以及它的管理算法 统称为 Fiber
。
html
下面是一个很是简单的小应用,接下来的整篇文章都将使用到它。一个按钮,点击数字加一,并渲染到屏幕上。java
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
的文本内容也随之更新。node
在 React 协调过程当中有各类各样的工做须要被执行。以上面代码为例,在 React 第一次渲染和随后的 state 更新期间的作了一些事情,大体以下:react
ClickCounter
组件 state
的 count
属性。ClickCounter
组件的孩子节点并对比他们的 props
。span
元素。还有一些在协调阶段执行的工做,如:生命周期方法调用或 refs 更新等。全部这些活动在 Fiber
架构中被总称为 ‘work’
。‘work’
的类型一般基于 React Element
的 type
。 例如,对类组件来讲须要 React 来建立实例,相对于函数组件来讲就没有必要了。众所周知,在 React 中有很的组件类型,类组件,函数组件(无状态组件),宿主组件(DOM),portals
等。React Element
的 type
由 createElement 函数的第一个参数定义。这个函数一般在 render
方法中被使用,用于建立上面提到的 React elements
。git
在咱们探索 ‘work’
和 Fiber
架构算法以前,咱们先熟悉下一在 React 内部使用的数据结构。github
在 React
中每个组件都有其对应的 UI 呈现,咱们称之为视图,或者也能够说是 render
方法返回的模板。例子 ClickCounter
组件的模板以下:算法
<button key="1" onClick={this.onClick}>Update counter</button>
<span key="2">{this.state.count}</span>
复制代码
当一个模板被传入到 JSX
编译器,最终会生成 React Element
。它其实就是 React
组件的 render
方法返回的实际内容,并非 HTML..,固然咱们也能够不使用 JSX
语法,也能够直接像下面代码示例中展现那样写组件 ,能接受的话,哈哈哈……:json
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
}
}
]
复制代码
在上面代码中你能够看到,React 为两个返回的数据对象都添加了 [$$typeof](https://overreacted.io/why-do-react-elements-have-typeof-property/)
属性,它在后会用到,主要是用来甄别是否为合法有效的 Element
。同时咱们可看到了type
, key
and props
三个属性。这里咱们须要要注意一下 React
是如何表示 span
和 button
两个节点的文本内容的,还有 button
节点的 onClick
部分。
对于示例组件 **ClickCounter**
来讲,它自身的 React Element
并无添加任何属性 或者 key
。
{
$$typeof: Symbol(react.element),
key: null,
props: {},
ref: null,
type: ClickCounter
}
复制代码
协调其实是将从组件 render
方法返回的每一个 React Element
合并进入fiber node
树的过程。每个 React Element
都有一个对应的 fiber
节点。不一样于 React Element
,fiber
并不会在每次渲染时候都从新建立。它们是可变的数据结构,保存着组件的状态,以及对应实体 DOM
节点的引用,这个下面会讲到。
咱们以前提到过,React
会基于 React Element
的 type
属性执行不一样工做。在示例程序中, ClickCounter
组件将调用生命周期函数 和 render
方法,而宿主组件 span
将会改变 DOM
内容。每个 React Element
都会转换为它所对应的 Fiber Node
,用于描述接下来要被执行的工做。
也就是说,能够认为 fiber
节点就是描述随后要执行工做的一种数据结构,能够类比于调用栈中的帧 ,换而言之,就是一个工做单元。fiber
架构同时还提供了便捷的方式去追踪,调度,暂停,中断协调进程。
当一个 React Element
第一次被转换为 fiber
节点的时候,React
将会从 React Element
种提取数据子并在在 createFiberFromTypeAndProps
函数中建立一个新的 fiber
。随后的更新过程当中 React
会复用这个建立的 fiber
节点,并将对应 React Element
被改变数据更新到这个 fiber
节点上。React 也会移除一些 fiber
节点,例如:当同层级上对应 key
属性改变时,或 render
方法返回的 React Element
没有该 fiber
对应的 Element
对象时,该 fiber
就会被删除。
由于 React
会为每一个获得的 React Element
建立 fiber
,这些 fiber
节点被链接起来组成 fiber tree
。在咱们的例子中它是这样的:
全部的 fiber
节点都经过属性 child
, sibling
and return
链接起来。
在第一次渲染完成后,React
最终生成 fiber tree
,它能够理解为渲染出的 UI 界面的在内存中的映射。fiber tree
的引用变量一般是 current
。当 React
开始更新操做时,它会又会构建一棵被称为 workInProgress
的新树。workInProgress
接下来将会替换 current
变量所引用的旧的 fiber tree
,而后随之会被刷新到屏幕上。
全部执行相关更新,删除等操做的 fibers
都来自于 workInProgress
树。当 React 遍历 current
树的时候,会为每一个 fiber node
建立一个备用节点,这些备用节点最终组成整个 workInProgress
树。这些被新建立的 fibers 的数据来自于 render
方法返回的 React Element
。一旦更新操做的工做完成,React
将会拥有一颗随时即可刷新到屏幕的备用树。当 workInProgress
树被刷新到屏幕后,那么它就会变成 current
树。也就是说 current
变量会保存这颗新树的指针。
React
的一个核心原则就是一致性。因此 React
会一次性遍更新全部须要处理的 DOM
,不会只显示部分结果。 workInProgress tree
对用户而言相似于一个不可见的草稿。因此 React 会先处理全部的组件内部状态的更新,而后再将它们改变了的部分从新刷新到屏幕上。
在源码中你会看到不少的函数从 current
and workInProgress
树 中获取 fiber
节点。每个 fiber
节点在 alternate
域中保存它在另外一棵树中对应节点的引用。也就是说一个来自 current
树的节点会指向 workInProgress
树中相对应的节点,反之亦然。
咱们能够认为一个 React
组件,它其实就是一个使用 state
和 props
来计算 UI 表现形式的函数。像 DOM
或 调用生命周期方法这样的活动都被认为是一个反作用。关于反作用的详情能够查看官方文档。
平常开发中,其实不少时候 state 和 props 的更新都会致使反作用的产生。每种反作用的应用其实是一种指定类型的工做,而 fiber
节点是一种方便的机制去追踪反作用,从它被添加直到被应用。每个 fiber node
都有会有与之相关的反作用。它们都被编码在fiber
节点的 effectTag
域上。
因此,Fiber
中的反作用基本上能够理解为,定义了在更新进程完成后须要对实例执行的具体操做。对宿主组件而言,也就是 DOM
,的这些工做包括 element
的添加,更新,移除。对的类组件来讲可能须要更新 refs
以及调用生命周期方法等。
React
的 DOM
更新速度很是快,为了实现这样的性能,它的实现上采用了一些有趣的技术,其中之一就是构建了一个能够高效迭代遍历的线性 fiber node
列表,该列表中全部 fiber node
都是有反作用须要被执行的,对于没有反作用的 fiber node
则没必要浪费宝贵的时间了。
这个列表的目的是标记那些须要执行各类反作用的节点,包括 DOM
更新,删除,生命周期函数调用等。它同时也是 finishedWork
树的子集,列表中的节点之间经过 nextEffect
属性链接。
Dan Abramov 对反作用列表作了一个有趣的比喻,他认为反作用列表像一颗圣诞树,全部的反作用节点被导线绑在一块儿。以下图:
React
经过
firstEffect
指针获得列表的起点。因此上面的图也能够理解为这样:
React
应用反作用的顺序是从子节点到父节点。
每个 React
应用都有一个或多个 DOM
元素做为容器,一般开发中那个 id
为 root
的元素。示例程序中的容器是 id
为 container
的 div
。
const domContainer = document.querySelector('#container');
ReactDOM.render(React.createElement(ClickCounter), domContainer);
复制代码
React 会为每个容器建立一个 fiber root
对象。 进入这个地址能够看到它的具体实现。
const fiberRoot = query('#container')._reactRootContainer._internalRoot
复制代码
fiber root
是 React
保存 fiber tree
引用的地方。也就是 fiber root
节点上的属性 current
。
const hostRootFiberNode = fiberRoot.current
复制代码
fiber tree
起点是一个被称之为 HostRoot
的特殊类型的 fiber node
。它在内部被建立,扮演着全部节点的祖先节点的角色。HostRoot
的属性 stateNode
反过来又指向 FiberRoot
。
fiberRoot.current.stateNode === fiberRoot; // true
复制代码
你能够探索你本身应用的中的 fiber tree
经过进入顶层的 fiberRoot
,进而获得 HostRoot
。或者你也能够经过组件实例获得单个 fiber
节点。
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 元素
{
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
这几个域的用处在以前的部分已经讲解过了,如今让咱们开始研究剩下的这些。
用于保存类组件的实例,宿主组件的 DOM 实例等。一般咱们也能够说这个属性是用来保存与该 fiber
相对应的的本地状态 。
定义了与该 fiber node
相对应的是一个函数组件仍是一个类组件。若是是一个类组件该属性指向这个类的构造函数。若是是一个 DOM 元素,该属性则是与之相对应的 HTML
标签。使用这个域很容易就能理解与该 fiber
节点相关联的元素是什么。
定义了当前 fiber
的类型。协调算法用它来判断具体须要作什么工做。像以前说的这个工做的类型仍是基于 React Element
的类型。createFiberFromTypeAndProps
函数映射一个 React Element 到与之相对应的 fiber node 类型。在咱们的示例中, ClickCounter
组件的属性 tag
是 1,表示 ClassComponent
, span
元素的 tag 是5 ,表示 HostComponent
。
一个状态更新队列,包括回调 和 DOM
更新。
已经被使用渲染的过的 fiber
状态。也就是当前屏幕上 UI 状态的映射。
已经使用渲染过的 fiber
属性,也是构成当前屏幕 UI 状态映射的一部分。
保存着最近一次从 render
方法返回的 React Element
中拿到的数据,等待随后被应用到子组件或是 DOM
元素上。
相同层级孩子节点惟一标记,能够优化提高 React 对子节点更新,添加,删处的判断效率。与它具体功能相关的官方文档能够看这里。reactjs.org/docs/lists-…
React 工做执行大体能够分为两个阶段:render
和commit
。
在 render
阶段,React 经过调度 setState
或者 React.render
来实现组件更新,同时也会计算出须要被更新的那部分 UI 的状态。若是是是初始化渲染,React
会为 render
方法返回的每个 React Element 建立一个 fiber 对象。在随后的更新中,若是当时建立 fiber
时对应的 Reatc Element
还存在,那么该 fiber
还会被复用,或者更新。这个阶段最后的成果就是在这颗 fiber
树上对标记了出了那些有反作用的 fiebr
节点。在接下来的 commit
阶段就是处理这些被标记节点反作用的节点 ,最后呈现为可视化的 UI。
有一件很重要的事情须要理解,那就是 render
阶段的执行能够是异步的。React 会处理一个或多个 fiber
节点 ,基于可利用的有效时间,若是有效时间用完它就会停下来,亦或者让位于优先级更高的事件,例如:用户点击操做等。随后再次找到它暂停的位置处继续它未完成的工做。但有时,则须要放弃已经执行过的工做,而后从头开始。这些暂停之因此成为多是由于它们执行的工做并不会形成用户可见的改变,像 DOM 更新之类的。相反,接下来的 commit
阶段则都是同步执行的。由于这个阶段全部执行的工做是反作用的应用,最主要的DOM 更新,会致使用户可见的改变。这就是为何 React
须要一次性执行完全部反作用的缘由。
调用生命周期方法也是反作用的一部分。一些方法在 render
阶段被调用,剩下的固然在 commit 阶段被调用啦。下面列举了在 render
阶段调用的生命周期方法:
从列表中你能够看到,不少在 render
阶段被调用的旧的生命周期方法在 version 16.3
及后面的版本中都被标记上了 UNSAFE
。如今在这篇文档中它们被统称为遗留生命周期方法。它们颇有可能会在将来的版本中被移除。
想知道缘由吗?
好吧,其实这其中的缘由与咱们上面学到的知识息息相关,首先咱们知道 render
阶段的更新不会有任何影响用户视觉上可见的的反作用产生,例如: DOM
更新之类的,甚至 React
还会异步的更新组件。然而这些被使用 UNSAFE
标记的生命周期方法常常会被开发者错误的使用,甚至滥用。开发者倾向于将包含反作用的代码放置到这些方法中,这样就可能致使新的异步渲染被触发,例如:在 componentWillUpdate
函数中调用 setState
方法就会致使错误产生。甚至程序无限循环,直至奔溃。UNSAFE
标记也是提醒开发者慎重使用。
接下来咱们讲讲 commit
阶段被调用的生命周期方法:
由于这些方法的执行的执行都是同步的,因此它们能够包含不少的反作用以及 DOM
操做。
ok,目前咱们已经掌握了足够的背景知识,接下来就能够潜入探究一番, React
用到的遍历和执行相关操做的算法。
协调算法的执行老是从顶层的 HostRoot
节点开始执行工做,经过调用 renderRoot
方法开始。然而 React
会跳过那些被处理过的 fiber
节点,直到找到还未被处理的节点。例如,若是你在组件树的某一处调用了 setState
,React
会从顶部开始遍历,可是会快速的跳过它的祖先节点,直到找到触发 setState
的组件为止。
全部的 fiebr
节点都在在循环遍历中被处理。咱们先看看循环算法实现的同步部分:
function workLoop(isYieldy) {
if (!isYieldy) {
while (nextUnitOfWork !== null) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
} else {...}
}
复制代码
代码中的 nextUnitOfWork
变量保存着从 workInProgress
树中取出须要被处理的 fiebr
节点的引用。当 React 遍历这个 Fiber
树的时候,它可使用这个变量的引用来不断检索遍历到剩下还未被处理的 fiber
节点。在当前的 fiber
节点被处理后,这个变量随后会指向下一个要被处理的 fiber
节点的引用,存在的话。不然为 null
。
在遍历树和初始化的过程当中重要用的的 4 个方法:
为了说明他们是如何被使用的,请看下面的 fiber
树遍历的示意动画:
注意:垂直方向上节点之间经过 siblings 属性链接,折线处表示链接孩子节点,经过 children 属性。
让咱们首先从 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
树的 fiber
节点做为参数,同时调用 beginWork
方法。一个 fiber
节点的全部须要执行的处理都开始于这个方法。为了证实这一点,咱们简单的在日志中记录那些已经完成功能处理的 fiber 节点的名字。 beginWork
方法老是会返回一个指向下一个将要被处理的孩子节点的指针,或者 null
。
若是存在下一个孩子节点,那么它将在循环中被赋值给 nextUnitOfWork
变量。然而要是返回了 null
,这时 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;
}
复制代码
该函数的主体是一个大的 while
循环。当 workInProgress
没有孩子节点的时候 React
就会进入这个函数。当完成当前遍历到的孩子节点的工做后,React 就检查是否有兄弟节点,若是有 React
会退出这个函数,并返回该节点兄弟节点的指针。它一样会被赋值给 nextUnitOfWork
变量,节点来 React
会重复上面的过程,直至整棵子树被遍历处理完成。当子节点以及子节点的孩子节点都被处理完成后,回溯至父节点,再重复循环父节点的兄弟节点,直至整棵树被遍历完成,最终返回到根节点 fiberRoot
。
这个阶段开始于 completeRoot
函数。在这个里 React
会更新 DOM
,调用相关生命周期方法。
当 React 进入这个阶段,它有两棵树和反作用列表。第一棵树就是是当前已经刷新到屏幕上 UI 对应的状态。另外一颗备用树就是在 render
阶段构建的,在源码中它一般称之为finishedWork
或 workInProgress
,在接下来的 commit
阶段会替换以前的旧树,将新的状态刷新到屏幕上。
finishedWork
树的上经过 nextEffect
指针链接的 fiber
节点构成反作用列表。_反作用列表能够看作是 render
阶段运行产生的成果。_渲染的意义就是去决定节点的插入,更新,删除,或是组件生命周期函数的调用。这些就是反作用列表将要告诉咱们的,也是接下来提交阶段须要遍历的节点集合。
在提交阶段运行的主函数是 commitRoot
,基本上它作了以下工做:
Snapshot
反作用的节点上调用 getSnapshotBeforeUpdate
生命周期方法。Deletion
反作用的节点上调用 componentWillUnmount
生命周期方法。DOM
插入,更新,删除操做。current
指针指向 finishedWork
树。Placement
反作用的组件节点上调用 componentDidMount
生命周期方法。Update
反作用的组件节点上调用 componentDidUpdate
生命周期方法。在 getSnapshotBeforeUpdate
调用后,React
会提交整棵树的全部反作用。整个过程分为两步。第一步执行 DOM
插入,更新,删除,ref
的卸载。接下来 React
将finishedWork
赋值给 FiberRoot
,并标记 workInProgress
树为 current
树。这样作的缘由是,第一步至关因而 componentWillUnmount
阶段,current
指向以前的树,而接下里的第二步则至关因而 componentDidMount/Update
阶段,current
要指向新树。
上面描述的主要执行函数:
function commitRoot(root, finishedWork) {
commitBeforeMutationLifecycles()
commitAllHostEffects();
root.current = finishedWork;
commitAllLifeCycles();
}
复制代码
每个子函数的实现都是遍历整个反作用列表,检查反作用的类型。当它找到须要它执行的反作用时就会执行应用。
下面是一个例子,这部分代码迭代了整个反作用列表,并检查循环到的节点是否有 Snapshot
反作用:
function commitBeforeMutationLifecycles() {
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
if (effectTag & Snapshot) {
const current = nextEffect.alternate;
commitBeforeMutationLifeCycles(current, nextEffect);
}
nextEffect = nextEffect.nextEffect;
}
}
复制代码
对于一个类组件来讲,这个反作用意味着调用 getSnapshotBeforeUpdate
生命周期方法。
React 执行DOM 更新使的是 commitAllHostEffects
函数。
function commitAllHostEffects() {
switch (primaryEffectTag) {
case Placement: {
commitPlacement(nextEffect);
...
}
case PlacementAndUpdate: {
commitPlacement(nextEffect);
commitWork(current, nextEffect);
...
}
case Update: {
commitWork(current, nextEffect);
...
}
case Deletion: {
commitDeletion(nextEffect);
...
}
}
}
复制代码
很是有趣,在 commitDeletion
函数中 React 调用 componentWillUnmount
是方法做为删除处理的一部分。
commitAllLifecycles
函数中 React 调用了全部剩下的生命周期方法,componentDidUpdate
, componentDidMount
译者添加
协调过程实际就是遍历整个 Fiber
树的时候,经过从 React Element
中获取到的改变后的数据,而后将这些数据更新到其所对应的 fiber
节点上,不存在对应的fiber
节点时,则建立新的,而后经过数据计算判断出该 fiber
节点在 commit
阶段须要作的事情,添加上对应的 effectTag
,同时该节点也会被添加到反作用列表中。在遍历完成以后会生成一棵新Fiber
树,该树中的 fiber
节点一些是新建立的,一些则是复用 old fiber tree
中的,具体状况取决于返回的 React Element
。在 commit
阶段就是遍历反作用列表并执行 effectTag
标记的工做。