本文同步发布在个人 Github博客javascript
最近在研究 React Fiber 相关的知识,上一篇文章 浅谈对 React Fiber 的理解 简单提到了 requestIdleCallback, React 源码中 polyfill 了这个方法,了解它对 Fiber 也能有进一步理解。本篇会深刻介绍下这个方法。前端
requestIdleCallback 是 window 属性上的方法,它的做用是在浏览器一帧的剩余空闲时间内执行优先度相对较低的任务。java
在网页运行中,有不少耗时但又不是那么重要的任务。这些任务和重要的任务如对用户的输入做出及时响应的之类的任务,它们共享事件队列。若是二者发生冲突,用户体验会很糟糕。react
requestIdleCallback 就解决了这个痛点,requestIdleCallback 会在每一帧结束时而且有空闲时间执行回调。git
假设须要大量涉及到 DOM 的操做的计算,在运算时,浏览器可能就会出现明显的卡顿行为,甚至不能进行任何操做,由于是 JS 单线程,就算用在输入处理,给定帧渲染和合成以后,用户的主线程就会变得空闲,直到下一帧的开始。github
而这些空闲时间能够拿来处理低优先级的任务,React16 的调度策略异步可中断,其中关键就靠的这个(polyfill)方法功能;React 把任务细分(时间切片),在浏览器空闲的时间去执行,从而尽量地提升渲染性能。redux
时间切片的本质是模拟实现 requestIdleCallback浏览器
讲到这里,从 React15 到 React16 Fiber,对总体性能来讲是大优化了;但要知道的是,React16 相对 15 作出的优化,并非大大减小了任务量,你写的代码的任务总量并无变化,只是把空闲时间利用起来了,不停的干活,就能更快的把活干完;这只是其中一个角度,React 还作了区分优先级执行等等。antd
当前大多数的屏幕刷新率都是 60HZ,一秒 60 帧(FPS 为 60),也就是每秒屏幕刷新 60 次,此时一帧的时间为 16.7ms(1000ms/60)低于 60HZ 人眼就会感知卡顿掉帧等状况。异步
浏览器的一帧说的就是一次完整的重绘。
前端浏览器所说的渲染频率 FPS(Frames Per Second)是每秒传输帧数,便是每秒刷新的次数,理论上 FPS 越高人眼以为界面越流畅。
window.requestIdleCallback()方法将在浏览器的空闲时段内调用的函数排队。这使开发者可以在主事件循环上执行后台和低优先级工做,而不会影响延迟关键事件,如动画和输入响应。函数通常会按先进先调用的顺序执行,然而,若是回调函数指定了执行超时时间 timeout,则有可能为了在超时前执行函数而打乱执行顺序。
你能够在空闲回调函数中调用 requestIdleCallback(),以便在下一次经过事件循环以前调度另外一个回调。
var handle = window.requestIdleCallback(callback[, options])
返回一个 ID 标识符。
callback: 一个在事件循环空闲时即将被调用的函数的引用。函数会接收到一个名为 deadline 的参数,这个参数能够获取当前空闲时间以及回调是否在超时时间前已经执行的状态;该对象上有两个属性: - timeRemaining:timeRemaining属性是一个函数,函数的返回值表示返回当前空闲时间的剩余时间 - didTimeout:didTimeout属性是一个布尔值,若是didTimeout是true,那么表示本次callback的执行是由于超时的缘由 options 可选 包括可选的配置参数。具备以下属性: timeout:若是指定了timeout并具备一个正值,而且还没有经过超时毫秒数调用回调,那么回调会在下一次空闲时期被强制执行,尽管这样极可能会对性能形成负面影响。
function myNonEssentialWork(deadline) { while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && tasks.length > 0) { doWorkIfNeeded() } if (tasks.length > 0) { requestIdleCallback(myNonEssentialWork) } } requestIdleCallback(myNonEssentialWork, 5000)
来看一个实际的例子:
requestIdleCallback(myWork) // 一个任务队列 let tasks = [ function t1() { console.log('执行任务1') }, function t2() { console.log('执行任务2') }, function t3() { console.log('执行任务3') }, ] // deadline是requestIdleCallback返回的一个对象 function myWork(deadline) { console.log(`当前帧剩余时间: ${deadline.timeRemaining()}`) // 查看当前帧的剩余时间是否大于0 && 是否还有剩余任务 if (deadline.timeRemaining() > 0 && tasks.length) { // 在这里作一些事情 const task = tasks.shift() task() } // 若是还有任务没有被执行,那就放到下一帧调度中去继续执行,相似递归 if (tasks.length) { requestIdleCallback(myWork) } }
个人运行结果以下(每次运行,不一样机器运行都不同):
当前帧剩余时间: 15.120000000000001 执行任务1 当前帧剩余时间: 15.445000000000002 执行任务2 当前帧剩余时间: 15.21 执行任务3
若是是由于 timeout 回调才得以执行的话,其实用户就有可能会感受到卡顿了,由于一帧的执行时间必然已经超过 16ms 了
这个方法理论上可行,但为何 React 团队又 polyfill 这个方法呢?
注意:timeRemaining 最大为 50ms,是有根据研究得出的,便是说人对用户输入的 100 毫秒之内的响应一般被认为是瞬时的,不会被人察觉到。将空闲时间限制在 50ms 内意味着即便在闲置任务开始后当即发生用户操做,用户代理仍然有剩余的 50ms 能够在其中响应用户输入而不会产生用户可察觉的滞后。
window.requestAnimationFrame() 告诉浏览器——你但愿执行一个动画,而且要求浏览器在下次重绘以前调用指定的回调函数更新动画。该方法须要传入一个回调函数做为参数,该回调函数会在浏览器下一次重绘以前执行
requestAnimationFrame 比起 setTimeout、setInterval 的优点主要有两点:
固然通常不会用 setTimeout 的,由于偏差很大
window.requestIdleCallback = function (handler) { let startTime = Date.now() return setTimeout(function () { handler({ didTimeout: false, timeRemaining: function () { return Math.max(0, 50.0 - (Date.now() - startTime)) }, }) }, 1) }
React 团队则是用 requestAnimationFrame 和 postMessage 模拟实现的,本篇就不讲了,有兴趣的能够去了解看看。
我近期会维护的开源项目: