在了解一个知识架构的时候,为了能更彻底的了解这个知识架构,我们通常会围绕着以下几个问题展开来理解。
- 什么是React Fiber
- React Fiber产生的原因(React Fiber解决了什么问题)
- 详细了解React Fiber
1.什么是fiber
首先fiber是操作系统的概念,即纤程。是更轻量级的线程,一个线程可以包含多个纤程。React在这里利用这个特点(注意是特点,意图相同)来命名React Fiber。
2.React Fiber产生的原因
Fiber诞生之前,React运行机制是采用一把梭的方式进行虚拟dom对比以及更新视图,大致过程如下
第一步:首先创建一颗虚拟dom tree
第二步:在状态改变的时候生成一颗新的虚拟dom tree
第三步:两个树进行对比得到更新的节点
第四步:将得到的更新节点对应生成真实dom
这种一把梭的流程弊端就是js执行时间过长导致UI卡顿甚至掉帧。至于为何UI卡顿或者掉帧呢~到这里就需要考虑一些关于浏览器原理问题。
浏览器原理
浏览器是多线程运行,包含JavaScript线程,事件线程,定时任务线程,HTTP请求线程,UI渲染线程等等,但是这里JS线程与UI线程是互斥的,
也就说是这俩只能执行其中一个线程,每当一个线程在执行的时候,另一个线程都会被挂起,原因很简单,JS线程可以操作dom,如果同时执行
会带来很多不可控问题。当JS线程执行时间过长的时候就会导致UI渲染线程一直被挂起,可操作时间就会一直推迟,导致的效果就是页面不响应,
卡顿,甚至掉帧的可能。
React Fiber出来之前,传统模式的React再进行协调(这里我只考虑这个阶段,因为映射真实dom阶段不会有太大的消耗)会有比较大的性能开销,
当然React在这个阶段已经做了这个优化,比如优化虚拟dom tree对比的过程。但在复杂场景仍然会导致这个问题。好了既然说到JS执行时间过长,那么
多少时间算合理呢~
通常设备的帧率是60次/秒,也就是一帧需要控制在16.67ms之内才能保证一个非常理想的交互体验,浏览器的一帧组成如同
从浏览器一帧的结构中可以观察到,渲染之间还有events,javascript,begin frame,rAF要处理,一帧中,需要将JS执行时间控制到一定范围,
才不会影响渲染。所以React Fiber主要的任务就是将传统React协调方式进行优化,从而达到每次运行可以控制在一个合理的范围,达到一个理
想的UI交互,这就是React Fiber要解决的问题。
ps:这里不考虑UI渲染部分的耗时。
3.详细了解React Fiber
React Fiber
(1)React Fiber 在reconciliation阶段的协调核心算法重新实现
(2)链表结构的Fiber tree,方便在reconciliation阶段做中断以及回溯
(3)可控调用栈
React Fiber重新实现
首先了解一帧的执行过程
在一帧中执行可以通过插入requestIdleCallback来执行定向操作,但是requestIdleCallback调用是在渲染之后,这就导致了,我们不可以在这里
再进行dom操作,再次操作dom会导致重新渲染,导致结果不可控。
来自MDN的解释:
window.requestIdleCallback()
方法插入一个函数,这个函数将在浏览器空闲时期被调用。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。函数一般会按先进先调用的顺序执行,然而,如果回调函数指定了执行超时时间timeout
,则有可能为了在超时前执行函数而打乱执行顺序。(来自:https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback)
React内部则是避开了requestIdleCallback采用window.requestAnimationFrame来动态调整帧率,计算JS剩余时间是否超过React设定的阈值16ms,从而实现
时间分片,原因就是requestAnmiationFrame再渲染之前执行,在这里做任何操作都不会影响到渲染的结果。React Fiber 规定一帧内JS执行的最大时间,即阈值16ms
来自MDN的解释:
window.requestAnimationFrame()
告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行
注意:若你想在浏览器下次重绘之前继续更新下一帧动画,那么回调函数自身必须再次调用window.requestAnimationFrame()
JS在执行过程,一个工作单元由JS引擎启动,一把梭得形式执行,只要开始就无法中断,直到整个工作单元执行结束。如图
React Fiber得目的是将一个繁重得任务进行拆分,拆分成一个个小的单元来进行执行,(这里就是fiber纤程得概念)按照优先级高低得方式
进行执行这一个个小的单元任务,这就是时间分片得概念。如下图
总结Fiber 大致调度过程
(1)state改变执行一个同步任务,将其拆分成一个任务队列
(2)在任务队列中按照任务优先级执行,如果执行超过了deadline,这状态设置为pending状态挂起
(3)一个fiber任务的执行或者挂起,根据requestIdleRequest/requestAnimationRequest实现的调度器,返一个新的fiber任务队列继续执行。
任务优先级
sync
InteractiveExpiration
AsyncExpiration
开发者视角
unstable_scheduleCallback(() => { // 异步低优先级任务,AsyncExpiration this.setState(); }); flushSync(() => { // 同步任务,最高优先级 this.setState(); }); onClick={() => { // 异步高优先级任务,InteractiveExpiration this.setState(); }}
Fiber节点
一个fiber结构是一个js对象,对应通过React.createElement来创建一个虚拟dom节点,整个虚拟dom树上得所有节点都对应一个fiber节点,这个fiber节点上
除了包含元素信息意外,还包含了一些其他信息,用来支撑整个调度算法。
fiber节点上重要得信息就是包含了三个“指针”,分别是child,sibling,return,用来遍历整个fiber树。
child指向第一个儿子节点(其余儿子节点都是基于第一个儿子节点通过sibling查找)
sibling指向兄弟节点
return指向父级节点
Fiber 树的结构图
得益于fiber节点指针以及整颗树得链表结构,可以实现任务分片,任务中断,按照优先级调度。
整个fiber树执行过程
render阶段
1创建当前current树
2状态改变生成workInProgress树
3协调生成effecList链表(变更得节点链表)
commit阶段
4将effectList链表映射到真实dom
Update更新过程
(1)记录节点状态
(2)将变更节点放到updateQueue中
(3)统一更新到UI视图层
setState干了什么
setState本质上只是一个改变state得命令,可以理解为是一个视图更新得请求,但是试图更新不是setState干得事儿。当执行了setState得时候
改变了state,得到变更得fiber节点,然后将变更得节点push到了updateQueue中,在React会统一在一个时间点处理整个updateQueue内得节点
我们可以观察到一个很常见得事儿,就是在调用setState方法之后state并没有及时更新。上述就是原因,举个例子
当在一个合成事件内同时执行了多个setState方法,比如针对一个count,多次进行了+1操作,得到得结果state只是1。原因就是,多次执行的时候
第一次执行setState的时候count进行+1的操作,但是state并没有及时改变,再次执行的时候state还是原来的值,这样多次操作其实效果就是一次操作
的效果,只是连续往updateQueue中push了count+1的第一次得到的值。