React Fiber 是 React v16.x 推出船新架构,而 Reconciliation 是 React 的 Diff 算法,二者都是 React 的 核心机制。本文将会来研究一下 React Fiber 和 Reconciliation,看看 Fiber 究竟是什么?Reconciliation 是如何基于 Fiber 运做的?Reconciliation 这个算法是怎么工做的?因为是 React 的核心机制,因此涉及到不少的概念和逻辑,烧烤哥也只能“轻轻地”烤一下,总结一些主要的原理,具体到源码层面的实现细节,还有待深刻研究(并且一篇文章的篇幅确定是说不清也说不完)。html
文章篇幅过长,建议收藏后观看前端
首先来一道 “开胃前菜”,让咱们来看看 DOM、Virtual DOM、React 元素、Fiber 对象的相关概念。node
文档对象模型,实际上就是一个树,树中的每个节点就是 HTML 元素(Element),每一个节点其实是一个 JS 对象,这个对象除了包含了该元素的一些属性,还提供了一些接口(方法),以便编程语言能够插入和操做 DOM。可是 DOM 自己并无针对动态 UI 的 Web 应用程序进行优化。所以,当一个页面有不少元素须要更新时,更新相对应的 DOM 会使得程序变得很慢。由于浏览器须要从新渲染全部的样式和 HTML 元素。这其实在页面没有任何变化的状况下也时常发生。react
为了优化“真实” DOM 的更新,衍生出了 「Virtual DOM」的概念。本质来讲,Virtual DOM 是真实 DOM 的模拟,它实际上也是一棵树。真实的 DOM 树由真实的 DOM 元素组成,而 Virtual DOM 树是由 Virtual DOM 元素组成。git
当 React 组件的状态发生变化时,会产生一棵新的 Virtual DOM 树,而后 React 会使用 diff 算法去比较新、旧两棵 Virtual DOM 树,获得其中的差别,而后将「差别」更新到真实的 DOM 树中,从而完成 UI 的更新。github
要说明一点是:这里并非说使用了 Virtual DOM 就能够加快 DOM 的操做速度,而是说 React 让页面在不一样状态之间变化时,使用了次数尽可能少的 DOM 操做来完成。算法
在 React 的世界中,React 给 Virtual DOM 元素取名为 React 元素(React Element)。也就是说,在 React 中的 Virtual DOM 树是由 React 元素组成的,树中每个节点就是一个 React 元素。编程
咱们从源码中(/package/shared/ReactElementType.js)来看看 React 元素的类型定义:数组
export type Source = {|
fileName: string,
lineNumber: number,
|};
export type ReactElement = {|
$$typeof: any,
type: any,
key: any,
ref: any,
props: any,
// ReactFiber
_owner: any,
...
|};
复制代码
$$typeof
:React 元素的标志,是一个 Symbol 类型;type
:React 元素的类型。若是是自定义组件(composite component),那么 type 字段的值就是一个 class 或 function component;若是是原生 HTML (host component),若是 div、span 等,那么 type 的值就是一个字符串('div'、'span');key
:React 元素的 key,在执行 diff 算法时会用到;ref
:React 元素的 ref 属性,当 React 元素变为真实 DOM 后,返回真实 DOM 的引用;props
:React 元素的属性,是一个对象;_owner
:负责建立这个 React 元素的组件,另外从代码中的注释 “// ReactFiber” 能够知道,它里面包含了 React 元素关联的 fiber 对象实例;当咱们写 React 组件时,不管是 class 组件仍是函数组件,return 的 JSX 会通过 JSX 编译器(JSX Complier)编译,在编译的过程当中,会调用 React.createElement()
这个方法。浏览器
当调用 React.createElement()
的时候实际上调用的是 ReactElement.js(/packages/react/src/ReactElement.js) 中的 createElement()
方法。调用这个方法后,会建立一个 React 元素。
在上面 ClickCounter 组件这个例子中, <button>
和 <span>
是 <ClickCounter>
的子组件。而 <ClickCounter>
组件其实它自己其实也是一个组件。它是 <App>
组件的子组件:
class App extends React.Component {
...
render() {
return [
<ClickCounter />
]
}
}
复制代码
因此在调用 <App>
的 render()
时,会建立 <ClickCounter>
组件对应的 react element:
当执行 ReactDOM.render()
后,建立的整棵 react rlement 树大体以下:
每当咱们建立一个 react element 时,还会建立一个与这个 react element 相关联的 fiber node。fiber node 为 Fiber 对象的实例。
Fiber 对象是一个用于保存「组件状态」、「组件对应的 DOM 的信息」、以及「工做任务 (work)」的数据结构,负责管理组件实例的更新、渲染任务、以及与其余 fiber node 的关系。每一个组件(react element)都有一个与之对应关联的 Fiber 对象实例(fiber node),和 react element 不同的是,fiber node 不须要再每一次界面更新的时候都从新建立一遍。
在执行 Reconciliation 这个算法的期间,组件 render 方法所返回的 react element 的信息(属性)都会被合并到对应的 fiber node 中。这些 fiber node 所以也组成了一棵与 react element tree 相对应的 fiber node tree。(咱们要紧紧记住的是:每一个 react element 都会有一个与之对应的 fiber node)。
Fiber 对象类型定义(/package/react-reconciler/src/ReactInternalTypes.js):
export type Fiber = {|
tag: WorkTag;
key: null | string;
type: any;
stateNode: any;
updateQueue: mixed;
memoizedState: any;
memoizedProps: any,
pendingProps: any;
nextEffect: Fiber | null,
firstEffect: Fiber | null,
lastEffect: Fiber | null,
return: Fiber | null;
child: Fiber | null;
sibling: Fiber | null;
...
|};
复制代码
tag
:这字段定义了 fiber node 的类型。在 Reconciliation 算法中,它被用于决定一个 fiber node 所须要完成的 work 是什么 ;key
:这个字段和 react element 的 key 的含义和内容有同样(由于这个 key 是从 react element 的key 那里直接拷贝赋值过来的),做为 children 列表中每个 item 的惟一标识。它被用于帮助 React 去计算出哪一个 item 被修改了,哪一个 item 是新增的,哪一个 item 被删除了。官方文档中有对 key 更详细的讲解;type
:这个字段表示与这个 fiber node 相关联的 react element 的类型。这个字段的值和 react element 的 type 字段值是同样的(由于这个 type 是从 react element 的 type 那里直接拷贝赋值过来的)。若是是自定义组件(composite component),那么 type 字段的值就是一个 class 或 function component;若是是原生 HTML (host component),如 div、span 等,那么 type 的值就是一个字符串('div'、'span');updateQueue
:这个字段用来存储组件状态更新、回调和 DOM 更新任务的队列,fiber node 正是经过这个字段,来管理 fiber node 所对应的 react element 的渲染、更新任务;(若是老铁们看过烧烤哥的那篇《烤透 React Hook》,就会知道,其实 updateQueue 存储的就是一个这个 fiber node 须要处理的 effect 链表);memoizedState
:已经被更新到真实 DOM 的 state(已经渲染到 UI 界面上的 state);memoizedProps
: 已经被更新到真实 DOM 的 props(已经渲染到 UI 界面上的 props),pendingProps
:等待被更新到真实 DOM 的 props;return
:这个字段至关于一个指针,指向父 fiber node;child
:这个字段至关于一个指针,指向子 fiber node;sibling
:这个字段至关于一个指针,指向兄弟 fiber node;nextEffect
:指向下一个带有 side-effect 的 fiber node;firstEffect
:指向第一个带有 side-effect 的 fiber nodelastEffect
:指向最后一个带有 side-effect 的fiber node;关于其余属性的解析,请看源码中的注释,或者这篇文章。
下面咱们来看看上面例子中的 <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>
的 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
}
复制代码
在上述 <ClickCounter>
的例子中,因为每一个组件的 react element 都会有一个与之对应的 fiber node,所以咱们会获得一棵 fiber node tree:
在 fiber node 的类型定义中,有三个属性:firstEffect、lastEffect 和 nextEffect,他们指向的是带有“side-effects”的 fiber node。那 "side-effect" 究竟是什么东西呢?写过 React 组件的老铁都知道,React 组件实际上就是一个函数,这个函数接收 props 和 state 做为输入,而后经过计算,最终返回 react element。在这个过程当中,会进行一些操做,例如更改 DOM 结构、调用组件的生命周期等等,React 把这些“操做”统称为「side-effect」,简称 「effect」,也就是常说的“反作用”。官方文档中有对 effect 进行介绍。
大部分组件的 state 和 props 的更新都会致使 side-effect 的产生。此外,咱们还能够经过 useEffect 这个 React Hook 来自定义一些 effect。在烧烤哥以前写的 《烤透 React Hook》一文中曾提到过,fiber node 的 effect 会以「循环链表」的形式存储,而后 fiber node 的 updateQueue 会指向这个 effect 循环链表。
在一个 fiber node tree 中,有一些 fiber node 是有 effect 须要处理的,而有一些 fiber node 是没有 effect 须要处理的。为了加快整棵 fiber node tree 的 effect 的处理速度,React 为那些带有 effect 须要处理的 fiber node 构建了一个链表,这个链表叫作 「effects list」。这个链表存储这那些带有 effect 的 fiber node。维护这个链表的缘由是:由于遍历一个链表比遍历一整棵 fiber node tree 的速度要快得多,对于那些没有 effect 须要处理的 fiber node,咱们没有必要花时间去迭代它。这个链表经过前面说过的 fiber node 的 firstEffect、lastEffect 和 nextEffect 三个属性来维护:firstEffect 指向第一个带有 effect 的 fiber node,lastEffect 指向最后一个带有 effect 的fiber node,nextEffect 指向下一个带有 effect 的 fiber node。
举个例子,下面有一个 fiber node tree,其中颜色高亮的节点时带有 effect 须要处理的 fiber node。假设咱们的更新流程将会致使 H 被插入到 DOM 中,C 和 D 将会改变自身的属性(attribute),G将会调用自身的生命周期方法等等。
那么,这个 fiber node tree 的 effect list 将会把这些节点链接到一块儿,这样,React 在遍历 fiber node tree 的时候能够跳过其余没有任何任务须要处理的 fiber node 了。
咱们再来回顾一下,React 对于页面上 UI 的一步步抽象转化:
一、任务调度器 Scheduler:决定渲染(更新)任务优先级,将高优的更新任务优先交给 Reconciler;当 class 组件调用 render() 方法(或者 function 组件 return)时,实际上并不会立刻就开始这个组件的渲染工做,此时只是会返回「渲染信息」(该渲染什么的描述),该描述包含了用户本身写的 React 组件(如 <MyComponent>
),还有平台特定的组件(如浏览器的 <div>
)。而后 React 会经过 Scheduler 来决定在将来的某个时间点再来执行这个组件渲染任务。
二、协调器 Reconciler:负责找出先后两个 Virtual DOM(React Element)树的「差别」,并把「差别」告诉 Renderer。关于 协调器是怎么运做的,咱们后面再来详细研究;
三、渲染器 Renderer:负责将「差别」更新到真实的 DOM 上,从而更新 UI;不一样的平台配会有不一样的 renderer。DOM 只是 React 可以适配的的渲染平台之一。其余主要的渲染平台还有 IOS 和安卓的视图层(经过 React Native 这个 renderer 来完成)。这种分离的设计意味着 React DOM 和 React Native 能使用独立的 renderer 的同时公用相同的,由 React core 提供的 reconciler。React Fiber 重写了 reconciler,但这事大体跟 rendering 无关。不过,不管怎么,众多 renderer 确定是须要做出一些调整来整合新的架构。
React 是一个用于构建用户界面的 JavaScript 类库。React 的核心机制是跟踪组件状态变化,而后将更新的状态映射到用户界面上。
使用 React 时,组件中 render() 函数的做用就是建立一棵 react element tree(React 元素树)。当调用 setState(),即下一个 state 或 props 更新时,render() 函数将会返回一棵不一样的 react element tree。接下来,React 将会使用 Diff 算法去高效地更新 UI,来匹配最近时刻的 React 元素树。这个 Diff 算法就是 Reconciliation 算法。
Reconciliation 算法主要作了两件事情:
在 React 15.x 版本以及以前的版本,Reconciliation 算法采用了栈调和器( Stack Reconciler )来实现,可是这个时期的栈调和器存在一些缺陷:不能暂停渲染任务,不能切分任务,没法有效平衡组件更新渲染与动画相关任务的执行顺序,即不能划分任务的优先级(这样有可能致使重要任务卡顿、动画掉帧等问题)。Stack Reconciler 的实现。
为了解决 Stack Reconciler 中固有的问题,以及一些历史遗留问题,在 React 16 版本推出了新的 Reconciliation 算法的调和器—— Fiber 调和器(Fiber Reconciler)来替代栈调和器。Fiber Reconciler 将会利用调度器(Scheduler)来帮忙处理组件渲染/更新的工做。此外,引入 fiber 这个概念后,原来的 react element tree 有了一棵对应的 fiber node tree。在 diff 两棵 react element tree 的差别时,Fiber Reconciler 会基于 fiber node tree 来使用 diff 算法,经过 fiber node 的 return、child、sibling 属性能更方便的遍历 fiber node tree,从而更高效地完成 diff 算法。
上文提到,Reconciliation 算法主要作了两件事情:
下面将围绕上述的“两件事情”,来看看 Reconciliation 算法是怎么运做的。
在对比两棵 react element tree 的时,React 制定了 3 个策略:
// 更新前
<div>
<p key="qianduan">前端</p>
<h3 key="shaokaotan">烧烤摊</h3>
</div>
// 更新后
<div>
<h3 key="shaokaotan">烧烤摊</h3>
<p key="qianduan">前端</p>
</div>
复制代码
假如没有 key,React 会认为 div 的第一个节点由 p 变为 h3,第二个子节点由 h3 变为 p。这符合第 2 个原则,所以会销毁并新建相应的 DOM 节点。
但当咱们加上了 key 属性后,便指明了节点先后的对应关系,React 知道 key 为 “shaokao” 的 p 在更新后还存在,因此 DOM 节点能够复用,只是须要交换一下顺序而已。
(关于 Diff 的源码:/packages/react-reconciler/src/ReactChildFiber.new.js)
根据上述的第一个策略“只对同级的 react element 对比”,意思就是只对同级的节点作对比。那“同级”的意思是什么呢?——直属于同一父节点的那些节点即为同级节点。举个例子:
如上图所示,新旧两棵树的根节点默认为同级节点:
具体到对比某个节点时,可分 2 种状况:
当对比得出节点(react element) 的 type 不相同时 ,React 会销毁原来的节点及其子孙节点,而后从新建立一个新的节点及其子孙节点。例如:
// 旧
<div>
<A />
<div>
<B />
<C />
</div>
</div>
// 新
<div>
<A />
<span>
<B />
<C />
</span>
</div>
复制代码
上面的例子中,原来的节点类型为 div,更新后节点的类型变为了 span,React 发现了这其中的不一样后,会销毁 div 节点及其子节点(B 和 C),而后从新建立一个类型为 span 的节点及其子节点(B 和 C)。
当对比得出节点(react element)的类型(type)相同时,React 会保留该 react element 对应的 DOM 节点(复用该 DOM),而后仅比对及更新有改变的属性(attribute)。例如:
// 旧
<div className="before" title="stuff" />
// 新
<div className="after" title="stuff" />
复制代码
经过对比,React 知道只须要修改 DOM 元素上的 className 属性便可。
当更新 style 属性时,React 仅更新有所改变的属性,例如:
// 旧
<div style={{color: 'red', fontWeight: 'bold'}} />
// 新
<div style={{color: 'green', fontWeight: 'bold'}} />
复制代码
经过对比,React 知道只须要修改 DOM 元素上的 color 样式,而无需修改 fontWeight。
以上所举的例子都是 Host Component(原生 HTML 元素),假如是对比相同类型的 Composite Component(本身写的 React 组件),此时主要看的是组件的 props 和 state 有没有改变,假若有改变,则更新组件及其子组件。
在对比同级节点(react element)时,有如下 2 种状况考虑:
这种状况相对简单,就是对比新旧两个节点而已,根据上面说的两种状况(节点类型相同、类型不一样)判断处理便可。
当同级有多个节点时,须要处理 3 种状况:
对于同级有多个节点的 Diff,必定属于以上三种状况中的一种或多种。React 团队发现,在平常开发中,相对于增长和删除,更新组件发生的频率更高,因此 React 的 Diff 算法会优先判断并处理节点的更新。
针对同级的多个节点,咱们能够将其看作是一个链表(由于实际上同级的 react element 它们各自对应的 fiber node 会经过 sibling 字段来链接成一个单向链表)。Diff 算法将会对「新同级节点链表」进行 2 次遍历:
(为了方便说明,如下将会分别把「旧 react element tree 的同级节点」和 「新 react element tree 的同级节点」称为「旧同级节点链表」和「新同级节点链表」)
以上流程的简单模拟代码以下(注意只是“简单模拟”的代码,和源码的具体实现仍是有区别的,若是想看源码具体的实现,请看 /packages/react-reconciler/srcReactChildFiber.new.js 的 reconcileChildArray()
函数):
// newNodeList 为 新同级节点链表
// oldNodeList 为 旧同级节点链表
for (let i = 0; i < newNodeList.length; i++) {
if (!oldNodeList[i]) break; // 若是「旧同级节点链表」已经遍历完了,则结束遍历
if (newNodeList[i].key=== oldNodeList[i].key &&
newNodeList[i].type === oldNodeList[i].type) {
continue; // 对应的 DOM 可复用,则继续遍历
} else {
break; // 对应的 DOM 不可复用,则结束遍历
}
}
复制代码
对于上述流程,当咱们结束遍历时,会有两种结果:
「结果一」:在步骤 3 结束了遍历,此时「新同级节点链表」和「旧同级节点链表」都没有遍历完。
看个例子:
// 旧
<li key="0">0</li>
<li key="1">1</li>
<li key="2">2</li>
// 新
<li key="0">0</li>
<li key="1">1</li>
<div key="2">2</div>
<li key="3">3</li>
复制代码
前面 key === 0,key === 1 的节点均可以复用,可是 到了 key === 2 时,因为节点的 type 发生了改变,所以对应的 DOM 不可复用,直接结束遍历。此时至关于「旧同级节点链表」中 key === 2 的节点未被遍历处理、「新同级节点链表」中 key ===二、 key === 3 的节点也没有被遍历处理。
「结果二」:若是是在步骤 4 结束遍历,那么多是 「新同级节点链表」遍历完、或者「旧同级节点链表」遍历完,又或者他们同时遍历完。例如:
// 旧
<li key="0" className="a">0</li>
<li key="1" className="b">1</li>
// 新
// 「新同级节点链表」和「旧同级节点链表」同时遍历完
<li key="0" className="aa">0</li>
<li key="1" className="bb">1</li>
复制代码
// 旧
<li key="0" className="a">0</li>
<li key="1" className="b">1</li>
// 新
//「新同级节点链表」没遍历完,「旧同级节点链表」就遍历完了
<li key="0" className="aa">0</li>
<li key="1" className="bb">1</li>
<li key="2" className="cc">2</li>
复制代码
// 旧
<li key="0" className="a">0</li>
<li key="1" className="b">1</li>
// 新
//「新同级节点链表」遍历完了,「旧同级节点链表」还没遍历完
<li key="0" className="aa">0</li>
复制代码
第二轮遍历时,主要是遍历「新同级节点链表」中剩下还没被遍历处理过的节点。
假如上一轮遍历结果为 「结果二」:
一、若是是「新同级节点链表」没有遍历完,「旧同级节点链表」已经遍历完的这种状况,则说明有节点新增,即将要新增的这个节点将会被打上一个 Placement
的标记 (newFiber.flags = Placement
)。
二、若是是「新同级节点链表」已经遍历完,「旧同级节点链表」没有遍历完的这种状况,则说明有节点须要被删除,这个即将要被删除的节点将会被打上一个 Deletion
的标记(returnFiber.flags |= Deletion
)。
假如上一轮遍历结果为 「结果一」:
假如为结果一,说明新、旧同级节点链表都没有遍历完,这意味着有的节点在此次更新中可能改变了位置!接下来是处理位置变换的节点。处理节点位置变换的 2 个主要思想就是:“剩下的节点中,哪些节点须要「右」移动?”、“移动到什么位置?”。
因为有节点交换了位置,因此咱们不能再经过节点的索引来对比新旧的节点了。不要慌,问题不大,咱们还能够利用 key
来将新旧的节点对应上。
在遍历「新同级节点链表」时,为了能快速在「旧同级节点链表」中找到对应的旧节点,React 会将「旧同级节点链表」中还没被处理过的节点以 map 的形式存放起来,其中 key
属性为 key,fiber node
为 value,这个 map 叫作 existingChildren
:
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
复制代码
existingChildren
是如何发挥做用的呢?在第二轮遍历时:
一、假如遍历到的「新同级节点」A 的 key 在 existingChildren
中能够找到,则说明在「旧同级节点链表」中能够找到一个和 A 的key 相同的「旧同级节点」A1。因为是经过 map 的实行来匹配的,很明确的一点就是 A 和 A1 的 key 是相同的,接下来就是判断它们的 type 是否相同:
二、假如遍历到的「新同级节点」A 的 key 在 existingChildren
中找不到,则说明在「旧同级节点链表」中找不到和 A 的 key 相同的「旧同级节点」A1,那就说明 A 是一个新增节点;
解决了 “新节点如何对应找到旧节点的问题” 后。接下来咱们来看看具体在第二轮循环的时候如何处理节点新增、删除、移动的。
其实新增和删除节点的状况很好理解,其实上面讲“两种结果”的时候已经说明了新增、删除的状况了。下面咱们重点来研究一下节点移动的状况。在前面曾经说过,处理节点的位置变化,主要抓住两个点:
以上两个问题实际上涉及到的是 方向 和 位移,若是想要明确这两个东西,就须要一个「基准点」,或者说「参考点」。React 使用 lastPlacedIndex
这个变量来存放「参考点」。咱们能够在源码的 reconcileChildrenArray()
函数的开头,看到:
let lastPlacedIndex = 0;
复制代码
lastPlacedIndex
这个变量表示当前最后一个可复用的节点,对应在「旧同级节点链表」中的索引。初始值为 0。(这个定义理解起来可能有点绕,不过不要紧,等下看两个例子就知道它究竟存的什么东西了)
在遍历剩下的「新同级节点链表」时,每个新节点会经过 existingChildren
找到对应的旧节点,而后就能够获得旧节点的索引 oldIndex
(即在「旧同级节点链表」中的位置)。
接下来会进行如下判断:
oldIndex
>= lastPlacedIndex
,表明该复用节点不须要移动位置,并将 lastPlacedIndex = oldIndex;oldIndex
< lastPlacedIndex
,表明该节点须要向右移动,而且该节点须要移动到上一个遍历到的新节点的后面;上述就是处理节点移动的逻辑。看完以后可能仍是有点懵,此时就须要配合一些栗子来服用,效果会更佳~
栗子1:
假设现有新旧两个同级节点列表(下列图中全部圆圈表明的节点的 type 均为 li,圈圈中的字母就是该节点的 key):
// 旧
<li key="a">a</li>
<li key="b">b</li>
<li key="c">c</li>
<li key="d">d</li>
// 新
<li key="a">a</li>
<li key="c">c</li>
<li key="d">d</li>
<li key="b">b</li>
复制代码
首先是第一轮循环:
第二轮循环:
刚刚第一遍循环只处理了第一个节点 a,目前「旧同级节点链表」中还有 b、c、d 还未被遍历处理,「新同级节点列表」中还有 c、d、b 还未被遍历处理。新、旧同级节点链表均没有完成遍历,也就是说,没有节点新增或删除,说明有节点变化了位置。所以接下来的第二轮循环,主要是处理节点的位置移动。在开始处理以前,先把「旧同级节点链表」中未被遍历处理的的 b、c、d 节点以 map 的形式存放到 existingChildren
中。
「新同级节点链表」遍历到节点 c:
「新同级节点链表」遍历到节点 d:
「新同级节点链表」遍历到节点 b:
第二轮遍历到此结束,最终,节点 a、c、d 对应的 DOM 节点都没有移动,而节点 b 对应的 DOM 则会被标记为“须要移动”。
因而,通过两轮循环后,React 就知道了,想要从「旧同级节点链表」变成「新同级节点链表」那样子,须要「旧同级节点链表」通过如下每一个节点的操做:
什么?感受只举一个栗子有点意犹未尽?咱们再来一个栗子~
假设现有新旧两个同级节点列表:
// 旧
<li key="a">a</li>
<li key="b">b</li>
<li key="c">c</li>
<li key="d">d</li>
// 新
<li key="d">d</li>
<li key="a">a</li>
<div key="b">b</div>
<li key="c">c</li>
复制代码
第一轮循环:
第二轮循环:
「新同级节点链表」遍历到节点 d:
「新同级节点链表」遍历到节点 a:
「新同级节点链表」遍历到节点 b:
「新同级节点链表」遍历到节点 c:
第二轮遍历到此结束。
通过两轮循环后,React 就知道了,想要从「旧同级节点链表」变成「新同级节点链表」那样子,须要「旧同级节点链表」通过如下每一个节点的操做:
上述每一个节点各自的“操做”(work)—— “移动到哪里”、“位置不变”、“插入新的,删掉旧的” 等等,会存放到节点各自对应的 fiber node 中。等到渲染阶段(Render phase)时,React 会读取并执行这些“操做”,从而完成 DOM 的更新。
小结:
咱们经过下面这样图来回顾一下整个 diff 流程:
通过上面的对比找出了「差别」以后,React 知道了“哪些 react element 要被删除”、“哪些 react element 须要添加子节点”、“哪些 react element 位置须要移动”、“哪些 react element 的属性须要更新”等等的一系列操做,这些操做会被看做一个个更新任务(work)。每一个 react element 自身的更新任务(work)会存储在与这个 react element 对应的 fiber node 中。
在 渲染阶段(Render phase),Reconciliation 会从 fiber node tree 最顶端的节点开始,从新对整棵 fiber node tree 进行 深度优先遍历,遍历树中的每个 fiber node,处理 fiber node 中存储的 work。遍历一次 fiber node tree 的执行其中的 work 的这个过程被称做一次 work loop。当一个 fiber node 本身和其全部子节点(child)分支上的 work 都被完成了,此时这个 fiber node 的 work 才算完成。一旦一个 fiber node 的 work 完成了,也就是说这个 fiber node 被结束了,而后 React 会接着去处理它的兄弟节点(silbing 字段所指向的 fiber node)的 work,在完成这个兄弟节点(sibling)的 work 后,就会继续移步到下一个兄弟节点......以此类推。当全部的 sibling 节点的 work 都处理完成后,React 才会回溯到 parent 节点(经过 return 字段一步步回溯)。这个过程发生在 completeUnitOfWork 函数
(/packages/react-reconciler/src/ReactFiberWorkLoop.new.js)中。
React 的开发者在这里作了一个优化(也就是前面提到过的 「Effect List」),React 会跳过那些已经处理过的 fiber node,只会去处理那些带有未完成 work 的 fiber node。举个例子,若是你在组件树的深层去调用 setState()
方法的话,那么 React 虽然仍是会从 fiber node tree 的顶部的节点开始遍历,可是它会跳过前面全部的父节点,直奔那个调用了 setState()
方法的子节点。
当 work loop 结束后(也就是遍历完整棵 fiber node tree 后),就会准备进入 commit 阶段(Commit phase)。在 commit 阶段,React 会去更新真实 DOM 树,从而完成 UI 的更新渲染。
(PS:因为篇幅有限,关于在 Render phase 和 Commit phase 两个阶段中更具体的流程以及在这个过程 fiber node 的每一个字段的做用和变化、还有 Scheduler、Renderer 的原理等等细节,彻底能够再写几篇文章了[笑哭],后面有机会在来做更深一步的研究和总结)
(基于 React v17.0.1 源码)
本文是烧烤哥基于 React 源码和网络上的一些文章总结而来,鉴于 React 内部机制复杂庞大和烧烤哥的能力有限,文中可能会出现错误或者总结得不够到位的地方,但愿各位老铁吃完烧烤以后在评论区指出,你们一块儿交流探讨,期待经过和老铁们的交流来加深对前端知识的理解。
关注「前端烧烤摊」 掘金 or 微信公众号, 第一时间获取烧烤哥前的总结与发现。