Fiber
在React16的新版本,使用了Fiber重新实现了React的核心算法,带来了杀手锏增量更新功能。它有能力将整个更新任务拆分为一个个小的任务,并且能控制这些任务的执行。 这些功能主要是通过两个核心的技术来实现的:
•新的数据结构 fiber
•调度器
这篇文章主要对调度器原理进行解析。
调度器简介
为什么需要调度?
大家都知道 JS 和渲染引擎是一个互斥关系。如果 JS 在执行代码,那么渲染引擎工作就会被停止。假如我们有一个很复杂的复合组件需要重新渲染,那么调用栈可能会很长。
调用栈过长,再加上如果中间进行了复杂的操作,就可能导致长时间阻塞渲染引擎带来不好的用户体验,可能浏览器会表现出卡顿、假死的情况,调度就是来解决这个问题的。
React 会根据任务的优先级去分配各自的 expirationTime,在过期时间到来之前先去处理更高优先级的任务,并且高优先级的任务还可以打断低优先级的任务(因此会造成某些生命周期函数多次被执行),从而实现在不影响用户体验的情况下去分段计算更新(也就是时间分片)。
React调度的实现
React主要由两部分实现:
1、计算任务的 expriationTime
2、实现 requestIdleCallback 的 polyfill 版本
expriationTime
expriationTime这个时间是用于帮助我们对比不同任务之间的优先级和计算任务的timeout(是否过期)。
计算公式: expriationTime=当前时间+一个常量(根据任务优先级改变)
当前时间指的是 performance.now(),这个 API 会返回一个精确到毫秒级别的时间戳(当然也并不是高精度的),另外浏览器也并不是所有都兼容 performance API 的。如果使用 Date.now() 的话那么精度会更差,但是为了方便起见,我们这里统一把当前时间认为是 performance.now()。
常量指的是根据不同优先级得出的一个数值,React 内部目前总共有五种优先级,数值越小优先级越高,分别为:
var ImmediatePriority = 1;
var UserBlockingPriority = 2;
var NormalPriority = 3;
var LowPriority = 4;
var IdlePriority = 5;
复制代码
它们各自的对应的数值都是不同的,具体的内容如下
var maxSigned31BitInt = 1073741823;
// Times out immediately
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
// Eventually times out
var USER_BLOCKING_PRIORITY = 250;
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
// Never times out
var
IDLE_PRIORITY = maxSigned31BitInt;
复制代码
也就是说,假设当前时间为 5000 并且分别有两个优先级不同的任务要执行。前者属于 ImmediatePriority,后者属于 UserBlockingPriority,那么两个任务计算出来的时间分别为 4999 和 5250(值越小就要优先执行)。通过这个时间可以比对大小得出谁的优先级高,也可以通过减去当前时间获取任务的 timeout。
requestIdleCallback
requestIdleCallback是一个web api接口,它会在浏览器空闲时期依次调用函数, 这就可以让开发者在主事件循环中执行后台或低优先级的任务,而且不会对像动画和用户交互这样延迟敏感的事件产生影响。函数一般会按先进先调用的顺序执行,然而,如果回调函数指定了执行超时时间timeout,则有可能为了在超时前执行函数而打乱执行顺序。如timeout 值被指定为正数时,当做浏览器调用 callback 的最后期限。它的单位是毫秒。当指定的时间过去后回调还没有被执行,那么回调会在下一次空闲时期被强制执行,尽管可能会对性能造成负面影响。
但是requestIdleCallback有一个致命的缺陷,它只能一秒调用回调 20次,这个满足不了现有的情况,所以React团队是自己实现这个函数。
实现 requestIdleCallback要点
实现requestIdleCallback主要是实现多次在浏览器空闲时且是渲染后才调用回调方法。
多次执行可以使用requestAnimationFrame,因为它是在浏览器的每一帧的重绘之前可以执行传入的函数,因此会比较准确。而在主流的浏览器中,浏览器刷新频率是60赫兹,一秒钟60次,就是一次耗时大概16毫秒。
如何判断浏览器当前是否处于空闲? 大家都知道在一帧当中,浏览器可能会响应用户的交互事件、执行 JS、进行渲染的一系列计算绘制。如果以上这些操作超过了 16ms,那么就会导致这一帧渲染没有完成并出现掉帧的情况,会造成页面有明显的卡顿,继而影响用户体验;如果以上这些操作没有耗时 16ms的话,那么我们就认为当下存在空闲时间让我们可以去执行任务。
计算方法见参考文献。
简单来说就是假设当前时间为 5000,浏览器支持 60 帧,那么 1 帧近似 16 毫秒,那么就会计算出下一帧时间为 5016。
得出下一帧时间以后,我们只需对比当前时间是否小于下一帧时间即可,这样就能清楚地知道是否还有空闲时间去执行任务。
最后,把我们需要在在渲染以后才去执行任务生成为一个宏任务,因为根据event loop是执行一个宏任务,再执行一个队列的微任务,因为放在宏任务是最合适。为了可以最快完成任务,放在MessageChannel来完成这个任务。
调度的流程
-
首先每个任务都会有各自的优先级,通过当前时间加上优先级所对应的常量我们可以计算出 expriationTime,高优先级的任务会打断低优先级任务
-
在调度之前,判断当前任务是否过期,过期的话无须调度,直接调用 port.postMessage(undefined),这样就能在渲染后马上执行过期任务了
-
如果任务没有过期,就通过 requestAnimationFrame 启动定时器,在重绘前调用回调方法
-
在回调方法中我们首先需要计算每一帧的时间以及下一帧的时间,然后执行 port.postMessage(undefined)
-
channel.port1.onmessage 会在渲染后被调用,在这个过程中我们首先需要去判断当前时间是否小于下一帧时间。如果小于的话就代表我们尚有空余时间去执行任务;如果大于的话就代表当前帧已经没有空闲时间了,这时候我们需要去判断是否有任务过期,过期的话不管三七二十一还是得去执行这个任务。如果没有过期的话,当前帧又没有时间,那就只能把这个任务丢到下一帧看能不能执行了
本文总体参考自yck,juejin.im/post/5cef53…
在原来的基础上加入了自己的理解