这是第 83 篇不掺水的原创,想获取更多原创好文,请搜寻公众号关注咱们吧~ 本文首发于政采云前端博客:前端工程师的自我涵养:React Fiber 是如何实现更新过程可控的
前言
从 React 16 开始,React 采纳了 Fiber 机制代替了原先基于原生执行栈递归遍历 VDOM 的计划,进步了页面渲染性能和用户体验。乍一听 Fiber 如同挺神秘,在原生执行栈都还没搞懂的状况下,又整出个 Fiber,还能不能欢快的写代码了。别慌,老铁!上面就来唠唠对于 Fiber 那点事儿。
什么是 Fiber
Fiber 的英文含意是“纤维”,它是比线程(Thread)更细的线,比线程(Thread)管制得更精细的执行模型。在狭义计算机科学概念中,Fiber 又是一种合作的(Cooperative)编程模型,帮忙开发者用一种【既模块化又合作化】的形式来编排代码。
简略点说,Fiber 就是 React 16 实现的一套新的更新机制,让 React 的更新过程变得可控,防止了之前一竿子递归到底影响性能的做法。
对于 Fiber 你须要晓得的基础知识
1 浏览器刷新率(帧)
页面的内容都是一帧一帧绘制进去的,浏览器刷新率代表浏览器一秒绘制多少帧。目前浏览器大多是 60Hz(60 帧 /s),每一帧耗时也就是在 16ms 左右。原则上说 1s 内绘制的帧数也多,画面体现就也细腻。那么在这一帧的(16ms)过程中浏览器又干了啥呢?
通过下面这张图能够分明的晓得,浏览器一帧会通过上面这几个过程:
- 承受输出事件
- 执行事件回调
- 开始一帧
- 执行 RAF (RequestAnimationFrame)
- 页面布局,款式计算
- 渲染
- 执行 RIC (RequestIdelCallback)
第七步的 RIC 事件不是每一帧完结都会执行,只有在一帧的 16ms 中做完了后面 6 件事儿且还有剩余时间,才会执行。这里提一下,如果一帧执行完结后还有工夫执行 RIC 事件,那么下一帧须要在事件执行完结能力持续渲染,所以 RIC 执行不要超过 30ms,如果长时间不将控制权交还给浏览器,会影响下一帧的渲染,导致页面呈现卡顿和事件响应不及时。
2. JS 原生执行栈
React Fiber 呈现之前,React 通过原生执行栈递归遍历 VDOM。当浏览器引擎第一次遇到 JS 代码时,会产生一个全局执行上下文并将其压入执行栈,接下来每遇到一个函数调用,又会往栈中压入一个新的上下文。比方:
function A(){B();
C();}
function B(){}
function C(){}
A();
引擎在执行的时候,会造成如下这样的执行栈:
浏览器引擎会从执行栈的顶端开始执行,执行结束就弹出以后执行上下文,开始执行下一个函数,直到执行栈被清空才会进行。而后将执行权交还给浏览器。因为 React 将页面视图视作一个个函数执行的后果。每一个页面往往由多个视图组成,这就意味着多个函数的调用。
如果一个页面足够简单,造成的函数调用栈就会很深。每一次更新,执行栈须要一次性执行实现,中途不能干其余的事儿,只能 ” 全心全意 ”。联合后面提到的浏览器刷新率,JS 始终执行,浏览器得不到控制权,就不能及时开始下一帧的绘制。如果这个工夫超过 16ms,当页面有动画成果需要时,动画因为浏览器不能及时绘制下一帧,这时动画就会呈现卡顿。不仅如此,因为事件响应代码是在每一帧开始的时候执行,如果不能及时绘制下一帧,事件响应也会提早。
3. 工夫分片(Time Slicing)
工夫分片指的是一种将多个粒度小的工作放入一个工夫切片(一帧)中执行的一种计划,在 React Fiber 中就是将多个工作放在了一个工夫片中去执行。
4. 链表
在 React Fiber 中用链表遍历的形式代替了 React 16 之前的栈递归计划。在 React 16 中应用了大量的链表。例如:
- 应用多向链表的模式代替了原来的树结构
例如上面这个组件:
<div id="id">
A1
<div id="B1">
B1
<div id="C1"></div>
</div>
<div id="B2">
B2
</div>
</div>
会应用上面这样的链表示意:
- 副作用单链表
- 状态更新单链表
- …
链表是一种简略高效的数据结构,它在以后节点中保留着指向下一个节点的指针,就如同火车一样一节连着一节
遍历的时候,通过操作指针找到下一个元素。然而操作指针时(调整程序和指向)肯定要小心。
链表相比程序构造数据格式的益处就是:
- 操作更高效,比方程序调整、删除,只须要扭转节点的指针指向就好了。
- 不仅能够依据以后节点找到下一个节点,在多向链表中,还能够找到他的父节点或者兄弟节点。
但链表也不是完满的,毛病就是:
- 比程序构造数据更占用空间,因为每个节点对象还保留有指向下一个对象的指针。
- 不能自在读取,必须找到他的上一个节点。
React 用空间换工夫,更高效的操作能够不便依据优先级进行操作。同时能够依据以后节点找到其余节点,在上面提到的挂起和复原过程中起到了关键作用。
React Fiber 是如何实现更新过程可控?
后面讲完基本知识,当初正式开始介绍明天的配角 Fiber,看看 React Fiber 是如何实现对更新过程的管控。
更新过程的可控次要体现在上面几个方面:
- 工作拆分
- 工作挂起、复原、终止
- 工作具备优先级
1. 工作拆分
后面提到,React Fiber 之前是基于原生执行栈,每一次更新操作会始终占用主线程,直到更新实现。这可能会导致事件响应提早,动画卡顿等景象。
在 React Fiber 机制中,它采纳 ” 化整为零 ” 的战术,将和谐阶段(Reconciler)递归遍历 VDOM 这个大工作分成若干小工作,每个工作只负责一个节点的解决。例如:
import React from "react";
import ReactDom from "react-dom"
const jsx = (
<div id="A1">
A1
<div id="B1">
B1
<div id="C1">C1</div>
<div id="C2">C2</div>
</div>
<div id="B2">B2</div>
</div>
)
ReactDom.render(jsx,document.getElementById("root"))
这个组件在渲染的时候会被分成八个小工作,每个工作用来别离解决 A1(div)、A1(text)、B1(div)、B1(text)、C1(div)、C1(text)、C2(div)、C2(text)、B2(div)、B2(text)。再通过工夫分片,在一个工夫片中执行一个或者多个工作。这里提一下,所有的小工作并不是一次性被切分实现,而是解决当前任务的时候生成下一个工作,如果没有下一个工作生成了,就代表本次渲染的 Diff 操作实现。
2. 挂起、复原、终止
再说挂起、复原、终止之前,不得不提两棵 Fiber 树,workInProgress tree 和 currentFiber tree。
workInProgress 代表以后正在执行更新的 Fiber 树。在 render 或者 setState 后,会构建一颗 Fiber 树,也就是 workInProgress tree,这棵树在构建每一个节点的时候会收集以后节点的副作用,整棵树构建实现后,会造成一条残缺的副作用链。
currentFiber 示意上次渲染构建的 Filber 树。在每一次更新实现后 workInProgress 会赋值给 currentFiber。在新一轮更新时 workInProgress tree 再从新构建,新 workInProgress 的节点通过 alternate 属性和 currentFiber 的节点建立联系。
在新 workInProgress tree 的创立过程中,会同 currentFiber 的对应节点进行 Diff 比拟,收集副作用。同时也会复用和 currentFiber 对应的节点对象,缩小新创建对象带来的开销。也就是说无论是创立还是更新,挂起、复原以及终止操作都是产生在 workInProgress tree 创立过程中。workInProgress tree 构建过程其实就是循环的执行工作和创立下一个工作,大抵过程如下:
当没有下一个工作须要执行的时候,workInProgress tree 构建实现,开始进入提交阶段,实现实在 DOM 更新。
在构建 workInProgressFiber tree 过程中能够通过挂起、复原和终止工作,实现对更新过程的管控。上面简化了一下源码,大抵实现如下:
let nextUnitWork = null;// 下一个执行单元
// 开始调度
function shceduler(task){nextUnitWork = task;}
// 循环执行工作
function workLoop(deadline){
let shouldYield = false;// 是否要让出工夫片交出控制权
while(nextUnitWork && !shouldYield){nextUnitWork = performUnitWork(nextUnitWork)
shouldYield = deadline.timeRemaining()<1 // 没有工夫了,检出控制权给浏览器}
if(!nextUnitWork) {conosle.log("所有工作实现")
//commitRoot() // 提交更新视图}
// 如果还有工作,然而交出控制权后, 申请下次调度
requestIdleCallback(workLoop,{timeout:5000})
}
/*
* 解决一个小工作,其实就是一个 Fiber 节点,如果还有工作就返回下一个须要解决的工作,没有就代表整个
*/
function performUnitWork(currentFiber){
....
return FiberNode
}
挂起
当第一个小工作实现后,先判断这一帧是否还有闲暇工夫,没有就挂起下一个工作的执行,记住以后挂起的节点,让出控制权给浏览器执行更高优先级的工作。
复原
在浏览器渲染完一帧后,判断以后帧是否有剩余时间,如果有就复原执行之前挂起的工作。如果没有工作须要解决,代表和谐阶段实现,能够开始进入渲染阶段。这样完满的解决了和谐过程始终占用主线程的问题。
那么问题来了他是如何判断一帧是否有闲暇工夫的呢?答案就是咱们后面提到的 RIC (RequestIdleCallback) 浏览器原生 API,React 源码中为了兼容低版本的浏览器,对该办法进行了 Polyfill。
当复原执行的时候又是如何晓得下一个工作是什么呢?答案在后面提到的链表。在 React Fiber 中每个工作其实就是在解决一个 FiberNode 对象,而后又生成下一个工作须要解决的 FiberNode。顺便提一嘴,这里提到的 FiberNode 是一种数据格式,上面是它没有开美颜的样子:
class FiberNode {constructor(tag, pendingProps, key, mode) {
// 实例属性
this.tag = tag; // 标记不同组件类型,如函数组件、类组件、文本、原生组件...
this.key = key; // react 元素上的 key 就是 jsx 上写的那个 key,也就是最终 ReactElement 上的
this.elementType = null; // createElement 的第一个参数,ReactElement 上的 type
this.type = null; // 示意 fiber 的实在类型,elementType 根本一样,在应用了懒加载之类的性能时可能会不一样
this.stateNode = null; // 实例对象,比方 class 组件 new 完后就挂载在这个属性下面,如果是 RootFiber,那么它下面挂的是 FiberRoot, 如果是原生节点就是 dom 对象
// fiber
this.return = null; // 父节点,指向上一个 fiber
this.child = null; // 子节点,指向本身上面的第一个 fiber
this.sibling = null; // 兄弟组件, 指向一个兄弟节点
this.index = 0; // 个别如果没有兄弟节点的话是 0 当某个父节点下的子节点是数组类型的时候会给每个子节点一个 index,index 和 key 要一起做 diff
this.ref = null; // reactElement 上的 ref 属性
this.pendingProps = pendingProps; // 新的 props
this.memoizedProps = null; // 旧的 props
this.updateQueue = null; // fiber 上的更新队列执行一次 setState 就会往这个属性上挂一个新的更新, 每条更新最终会造成一个链表构造,最初做批量更新
this.memoizedState = null; // 对应 memoizedProps,上次渲染的 state,相当于以后的 state,了解成 prev 和 next 的关系
this.mode = mode; // 示意以后组件下的子组件的渲染形式
// effects
this.effectTag = NoEffect; // 示意以后 fiber 要进行何种更新
this.nextEffect = null; // 指向下个须要更新的 fiber
this.firstEffect = null; // 指向所有子节点里,须要更新的 fiber 里的第一个
this.lastEffect = null; // 指向所有子节点中须要更新的 fiber 的最初一个
this.expirationTime = NoWork; // 过期工夫,代表工作在将来的哪个工夫点应该被实现
this.childExpirationTime = NoWork; // child 过期工夫
this.alternate = null; // current 树和 workInprogress 树之间的互相援用
}
}
额…看着如同有点上头,这是开了美颜的样子:
是不是难看多了?在每次循环的时候,找到下一个执行须要解决的节点。
function performUnitWork(currentFiber){//beginWork(currentFiber) // 找到儿子,并通过链表的形式挂到 currentFiber 上,每一偶儿子就找前面那个兄弟
// 有儿子就返回儿子
if(currentFiber.child){return currentFiber.child;}
// 如果没有儿子,则找弟弟
while(currentFiber){// 始终往上找
//completeUnitWork(currentFiber);// 将本人的副作用挂到父节点去
if(currentFiber.sibling){return currentFiber.sibling}
currentFiber = currentFiber.return;
}
}
在一次工作完结后返回该解决节点的子节点或兄弟节点或父节点。只有有节点返回,阐明还有下一个工作,下一个工作的解决对象就是返回的节点。通过一个全局变量记住当前任务节点,当浏览器再次闲暇的时候,通过这个全局变量,找到它的下一个工作须要解决的节点复原执行。就这样始终循环上来,直到没有须要解决的节点返回,代表所有工作执行实现。最初大家手拉手,就造成了一颗 Fiber 树。
终止
其实并不是每次更新都会走到提交阶段。当在和谐过程中触发了新的更新,在执行下一个工作的时候,判断是否有优先级更高的执行工作,如果有就终止原来将要执行的工作,开始新的 workInProgressFiber 树构建过程,开始新的更新流程。这样能够防止反复更新操作。这也是在 React 16 当前生命周期函数 componentWillMount 有可能会执行屡次的起因。
3. 工作具备优先级
React Fiber 除了通过挂起,复原和终止来管制更新外,还给每个任务分配了优先级。具体点就是在创立或者更新 FiberNode 的时候,通过算法给每个任务分配一个到期工夫(expirationTime)。在每个工作执行的时候除了判断剩余时间,如果以后解决节点曾经过期,那么无论当初是否有闲暇工夫都必须执行改工作。
同时过期工夫的大小还代表着工作的优先级。
工作在执行过程中顺便收集了每个 FiberNode 的副作用,将有副作用的节点通过 firstEffect、lastEffect、nextEffect 造成一条副作用单链表 AI(TEXT)-B1(TEXT)-C1(TEXT)-C1-C2(TEXT)-C2-B1-B2(TEXT)-B2-A。
其实最终都是为了收集到这条副作用链表,有了它,在接下来的渲染阶段就通过遍历副作用链实现 DOM 更新。这里须要留神,更新实在 DOM 的这个动作是零打碎敲的,不能中断,不然会造成视觉上的不连贯。
对于 React Fiber 的思考
1. 是否应用生成器(generater)代替链表
在 Fiber 机制中,最重要的一点就是须要实现挂起和复原,从实现角度来说 generator 也能够实现。那么为什么官网没有应用 generator 呢?猜想应该是是性能方面的起因。生成器不仅让您在堆栈的两头退让,还必须把每个函数包装在一个生成器中。一方面减少了许多语法方面的开销,另外还减少了任何现有实现的运行时开销。性能上远没有链表的形式好,而且链表不须要思考浏览器兼容性。
2. Vue 是否会采纳 Fiber 机制来优化简单页面的更新
这个问题其实有点搞事件,如果 Vue 真这么做了是不是就是变相抵赖 Vue 是在 ” 集成 ” Angular 和 React 的长处呢?React 有 Fiber,Vue 就肯定要有?
两者尽管都依赖 DOM Diff,然而实现上且有区别,DOM Diff 的目标都是收集副作用。Vue 通过 Watcher 实现了依赖收集,自身就是一种很好的优化。所以 Vue 没有采纳 Fiber 机制,也无伤大雅。
总结
React Fiber 的呈现相当于是在更新过程中引进了一个中场指挥官,负责掌控更新过程,足球世界里管这叫前腰。抛开带来的性能和效率晋升外,这种“化整为零”和工作编排的思维,能够利用到咱们平时的架构设计中。
招贤纳士
政采云前端团队(ZooTeam),一个年老富裕激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 40 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员形成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端利用、数据分析及可视化等方向进行技术摸索和实战,推动并落地了一系列的外部技术产品,继续摸索前端技术体系的新边界。
如果你想扭转始终被事折腾,心愿开始能折腾事;如果你想扭转始终被告诫须要多些想法,却无从破局;如果你想扭转你有能力去做成那个后果,却不须要你;如果你想扭转你想做成的事须要一个团队去撑持,但没你带人的地位;如果你想扭转既定的节奏,将会是“5 年工作工夫 3 年工作教训”;如果你想扭转原本悟性不错,但总是有那一层窗户纸的含糊… 如果你置信置信的力量,置信平凡人能成就不凡事,置信能遇到更好的本人。如果你心愿参加到随着业务腾飞的过程,亲手推动一个有着深刻的业务了解、欠缺的技术体系、技术发明价值、影响力外溢的前端团队的成长历程,我感觉咱们该聊聊。任何工夫,等着你写点什么,发给 ZooTeam@cai-inc.com