requestIdleCallback和requestAnimationFrame

661 阅读2分钟

页面流畅和FPS

  • 页面是一帧一帧绘制出来的,当每秒绘制的帧数(FPS)达到 60 时,页面是流畅的,小于这个值时,用户会感觉到卡顿。

  • 1s 60帧,所以每一帧分到的时间是 1000/60 ≈ 16 ms。所以我们书写代码时力求不让一帧的工作量超过 16.7ms

FPS

我们看下浏览器每一帧都处理什么

浏览器.png

  • 1.响应交互
  • 2.JS解析执行
  • 3.帧处理开始,窗口尺寸变化、窗口滚动等
  • 4.执行requestAnimationFrame(rAF)
  • 5.布局(重构/回流)
  • 6.绘制(重绘)

如果上面的6步完成后,还有剩余时间,就会执行requestIdleCallback的回调函数

requestAnimationFrame

先看看使用方法

var ele = document.getElementById("test")
var progress = 0;
function step(timestamp) {
    progress++;
    ele.style.width = progress + "%";
    if (progress < 100) {
        requestAnimationFrames(step)
    }
}
requestAnimationFrames(step)

上面这个例子,id为test的元素,width从0%增长到100%

API

window.requestAnimationFrame(callback)
  • callback:回调函数,接收一个参数(DOMHighResTimeStamp)

    • DOMHighResTimeStamp: 表示开始执行这个回调函数的时刻

和setTime、setInterval比较

在没有requestAnimation方法之前,可以使用setTime或setInterval来触发视觉变化,但是这种方法的问题:回调函数执行的时间是不固定的,可能刚好就在末尾,或者直接就不执行了,经常会引起丢帧而导致页面卡顿

setTimeout.jpeg

  • 时机不同

    • setTimeout/setInterval:使用定时器来触发回调函数,而定时器并无法保证能够准确无误的执行。比如同步代码执行太久。如果定时器时间间隔过短,会导致过度渲染。过长又会延迟渲染,导致不流畅
    • requestAnimationFrame:是由系统来决定回调函数的执行时机,会请求浏览器在下一次重新渲染之前执行回调函数。时间间隔会紧跟屏幕刷新一次需要的时间。但是要注意:如果回调(也就是这一帧)中执行太多任务,还是会造成卡顿

总结

  • 通常用来执行一个动画,浏览器下次重绘之前调用回调函数更新动画
  • 如果浏览器不支持这个方法,可以用setTimeout模拟
  • 每一帧渲染都执行一次
  • 系统决定调用回调函数,时间间隔固定

兼容

if (!window.requestAnimationFrame) {
    var lastTime = 0;
    window.requestAnimationFrame = function(callback) {
        // 当前时间
        var currTime = new Date().valueOf();
        // 时间间隔
        var interval = Math.max(0, 16.7 - (currentTime - lastTime))
        var id = window.setTimeout(function() {
            // 执行回调函数,并把执行的时间作为参数传回去
            callback(currTime + interval)
        }, interval)
        lastTime = currTime + interval;
        return id;
    }
}

requestIdleCallback

先来个例子,看下使用方法

var tasksNum = 10000

function work(deadline) {
  while (deadline.timeRemaining() && tasksNum > 0) {
    console.log(`执行了${10000 - tasksNum + 1}个任务`)
    tasksNum--
  }

  if (tasksNum > 0) { // 在未来的帧中继续执行
    requestIdleCallback(unImportWork)
  }
}

requestIdleCallback(work)

API

var handle = window.requestIdleCallback(callback[, options])
  • callback: 接受的回调函数,回调函数里面有一个参数(IdleDeadline)包含:

    • timeRemaining():当前帧剩余的空闲时间
    • didTimeout: boolean值,表示是否已经超时了,这个会根据传入的第二个参数决定
  • options: 对象只有一个参数

    • timeout: 毫秒,超时时间,如果超过这个时间,就会强制执行,不会等待空闲

总结

  • 一些低优先级的任务可以使用requestIdleCallback,但是同时因为时间有限,接受的任务尽量是可以很快执行的同步或者微任务
  • 因为是每一帧结束后执行,已经渲染完成,所以这个时候尽量不要操作DOM,减少不必要的重复渲染
  • Promise也不建议在这里执行,因为回调是微任务,会很快执行,时间不可控,可能会超过一帧时间,造成卡顿

区别

  • 是否执行

    • requestAnimationFrame:每一帧都会执行
    • requestIdleCallback:只有在每一帧执行完后,有剩余时间,才会执行,是捡浏览器空闲时间执行,所以不一定会执行。也就是如果浏览器一直非常的忙碌,回调方法可能永远不会执行

参考网址: 网址1网址2