手写requestIdleCallback

为什么要手写requestIdleCallback
requestIdleCallback兼容性

从上图可以看出手写的原因是:requestIdleCallback兼容性并不好

requestIdleCallback是用来干什么的呢?

是一个当浏览器处于闲置状态时,调度工作的新的性能相关的API

requestIdleCallback

通过上图可看到,一帧内需要完成如下六个步骤的任务:

  • 处理用户的交互
  • JS 解析执行
  • 帧开始。窗口尺寸变更,页面滚去等的处理
  • requestAnimationFrame(rAF)
  • 布局
  • 绘制
requestIdleCallback

上面六个步骤完成后没超过 16 ms,说明时间有富余,此时就会执行 requestIdleCallback 里注册的任务。


图一

从上图也可看出,requestIdleCallback 是捡浏览器空闲来执行任务。

如此一来,假如浏览器一直处于非常忙碌的状态,requestIdleCallback 注册的任务有可能永远不会执行。此时可通过设置 timeout (见下面 API 介绍)来保证执行。

总结:

方法将在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。函数一般会按先进先调用的顺序执行,然而,如果回调函数指定了执行超时时间timeout,则有可能为了在超时前执行函数而打乱执行顺序。

下面我们将基于 MessageChannelrequestAnimationFrame手写requestIdleCallback

要想通透的学会(必须将代码复制下来手一遍,如果看不懂注释留言评论)

    // 简单睡眠 
    function sleep(duration) {
      let start = Date.now();
      while (start + duration > Date.now()) { }
    }
    const works = [
      () => {
        console.log('A1开始');
        sleep(20);
        console.log('A1结束');
      },
      () => {
        console.log('A2开始');
        sleep(0);
        console.log('A2结束');
      },
      () => {
        console.log('A3开始');
        sleep(0);
        console.log('A3结束');
      },
      () => {
        console.log('A4开始');
        sleep(0);
        console.log('A4结束');
      },
      () => {
        console.log('A4开始');
        sleep(0);
        console.log('A4结束');
      }
    ]
    let channle = new MessageChannel();
    var activeFrameTime = 1000 / 60; // 16.6 表示每帧
    let frameDeadline; // 这一帧的截至时间
    let pendingCallback; // 此变量用来保存即将执行的函数
    let timeRemaining = _ => frameDeadline - performance.now();
    // 监听channle.port1.postMessage 消息
    channle.port2.onmessage = function () {
      let currentTime = performance.now();
      //如果帧的截止时间已经小于当前时间,说明已经过期了
      let didTimeout = frameDeadline <= currentTime;
      if (didTimeout || timeRemaining() > 0) {
        if (pendingCallback)
          pendingCallback({ didTimeout, timeRemaining });
      }
    }
    // 注册requestIdleCallback
    window.requestIdleCallback = (callback, options) => {
      /**
       * rafTime —— 该参数与performance.now()的返回值相同
       */
      requestAnimationFrame(rafTime => {
        //每一帧开始的时间加上16.6=就是一帧的截止时间了
        frameDeadline = rafTime + activeFrameTime;
        pendingCallback = callback;
        //其实发消息之后相当于添加一个宏任务
        channle.port1.postMessage('hello');
      })
    }
    //告诉浏览器,你可以在空闲的时间执行任务,但是如果已经过期了,不管你有没有空,都要帮我执行
    requestIdleCallback(workLoop, { timeout: 1000 });
    /**
     * 循环执行工具
     * @params {Object} deadline
     * deadline —— {didTimeout,timeRemaining}
     * didTimeout —— 是否到了将控制权归还给浏览器的时间
     * timeRemaining() 本帧的剩余时间
     */
    function workLoop(deadline) {
      console.log('本帧的剩余时间', parseInt(deadline.timeRemaining()));
      //如果说还有剩余时间或者此任务已经过期了 并且还有没有完成的任务
      while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && works.length > 0) {
        performUnitOfWork();
      }
      if (works.length > 0) {
        console.log(`只剩下${deadline.timeRemaining()},时间片已经到期了,等待下次调度`);
        requestIdleCallback(workLoop);
      }
    }
    //取出工作数组中的第一个工作并执行
    function performUnitOfWork() {
      works.shift()();  // 等价于  let work = works.shift(); work();
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。