在React16的新版本,使用了Fiber从新实现了React的核心算法,带来了杀手锏增量更新功能。它有能力将整个更新任务拆分为一个个小的任务,而且能控制这些任务的执行。 这些功能主要是经过两个核心的技术来实现的:web
•新的数据结构 fiber算法
•调度器api
这篇文章主要对调度器原理进行解析。浏览器
你们都知道 JS 和渲染引擎是一个互斥关系。若是 JS 在执行代码,那么渲染引擎工做就会被中止。假如咱们有一个很复杂的复合组件须要从新渲染,那么调用栈可能会很长。bash
调用栈过长,再加上若是中间进行了复杂的操做,就可能致使长时间阻塞渲染引擎带来很差的用户体验,可能浏览器会表现出卡顿、假死的状况,调度就是来解决这个问题的。数据结构
React 会根据任务的优先级去分配各自的 expirationTime,在过时时间到来以前先去处理更高优先级的任务,而且高优先级的任务还能够打断低优先级的任务(所以会形成某些生命周期函数屡次被执行),从而实如今不影响用户体验的状况下去分段计算更新(也就是时间分片)。函数
React主要由两部分实现:oop
一、计算任务的 expriationTimepost
二、实现 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…
在原来的基础上加入了本身的理解