使用requestIdleCallback调度任务

1,043 阅读4分钟

任务调度系统

操作系统中调度

操作系统中,基于时间片调度,如果到了时间该程序还没有运行完毕,那么此时会有中断,将该程序的相关运行位置,以及上下文环境等存入相关的寄存器中,之后进行恢复。

浏览器中调度

我们在浏览器中,也可以近似的模拟出调度算法,但是js程序必须自己守时,如果到了时间不自己退出,或者是长时间占用主线程,那么卡顿是必然的,所以在浏览器中必须来手动尽可能的控制程序的运行。

浏览器中目前提供的方式

在一个EventLoop,如果当前这个循环需要更新,js代码-requestAnimationFrame-UI渲染(layout, paint)-requestIdleCallback(0个或多个)-macroTask

requestIdleCallback

当浏览器运行时,主线程有空闲的时间,那么就会去运行回调,但是浏览器空闲下来,谁也不能保证接下来浏览器会发生什么,会不会有优先级更高的任务进来,如用户点击的交互事件,或者是动画事件,相关的研究发现,人在50ms内是不太容易察觉出变化来的,所以如果当前线程空闲(用户不操作,或者当前并没什么操作进行)当前requestIdleCallback会设定<50ms执行时间。

requestIdleCallback(fn, timeout?)

  • fn最终回调执行fn(deadline),deadline.timeout是否超时,deadline.timeRemaining()剩余时间,>=0, 到0表示已经执行超时了。
  • timeout,表示超时时间,因为rIC并不一定会执行,如果浏览器长时间忙碌,那么他会在timeout超时下强制执行

React的polyfill

React@16.6.x

React团队考虑到浏览器兼容问题,并没有使用rIC,而是使用rAF+setTimeout来进行polyfill

  • rAF,requestAnimationFrame用来在每个渲染时,发出执行的信号

并且其调用callback时,会传入类似当前performace.now,他使用计算屏幕的刷新率(多次rAF间隔)计算出下一次渲染的deadline当然这里包括了UI线程的时间,deadline与当前时间now可近似算出,这一帧还有多少的执行时间。

  • setTimeout同样是为了防止该任务在浏览器繁忙时,多次得不到执行而设定的一个超时

  • rAF中发出postMessage,UI渲染结束后,调用macroTask,监听message回调的事件会执行,并通过上下文计算是否超时

使用rIC(requestIdleCallback)小例子

使用chrome时,可在Performance调低性能,效果较为明显。

执行一定数量的console.log操作,使用同步异步来执行

模拟浏览器忙碌的情况

使用一个css动画模拟浏览器的忙碌情况,从而更好的测试性能,我们创建了一个左右移动的box

<style>
/* 使用margin移动,更好验证效果,如果做动画,transform还是首选 */
@keyframes slide {
  0% {
    margin-left: 0;
    /* transform: translateX(0); */
  }

  50% {
    margin-left: 200px;
    /* transform: translateX(200px); */
  }

  100% {
    margin-left: 0;
    /* transform: translateX(0); */
  }
}

.box {
  width: 400px;
  height: 200px;
  animation-duration: 3s;
  animation-name: slide;
  animation-iteration-count: infinite;
  background: red;
}
</style>

<div class="box"></box>

同步

const TEST_SIZE = 1000

function performSync () {
  const arr = new Array(TEST_SIZE)

  let i = 0;

  console.time('push')
  // 放入数组中
  for (; i < TEST_SIZE; i++) {
    arr[i] = i
  }
  i = 0
  console.timeEnd('push') // push: 0.41015625ms


  console.time('sync-log')
  // 打印
  for (; i < TEST_SIZE; i++) {
    console.log(arr[i])
  }
  i = 0
  console.timeEnd('sync-log') // sync-log: 2690.320068359375ms
}

可以看到,执行performSync时,浏览器动画发生了明显的卡顿,也即js占用时间过长,导致后面出现掉帧

异步

我们这里存取数据时,需要使用链表进行操作了,可能会涉及到频繁的增添元素,

function performAsync () {
  let deadline = null
  let firstCallbackNode = null
  let lastCallbackNode = null
  const arr = new Array(TEST_SIZE)
  let i = 0

  console.time('push')
  // 放入链中
  let obj = {}
  for (; i < TEST_SIZE; i++) {
    obj = { next: null, payload: i }
    if (firstCallbackNode === null) {
      lastCallbackNode = firstCallbackNode = obj
    }else {
      lastCallbackNode = lastCallbackNode.next = obj
    }
  }
  i = 0
  console.timeEnd('push') // push: 1.015869140625ms

  // 执行链表中的任务
  function flushWork(callback) {
    while (deadline.timeRemaining() > 0) {
      // 结束任务
      if (firstCallbackNode === null) {
        lastCallbackNode = null
        return
      }

      callback(firstCallbackNode.payload)
      firstCallbackNode = firstCallbackNode.next
    }
  }

  // 进行调度
  function scheduleWork(deadlineObj) {
    deadline = deadlineObj

    // 结束
    if (firstCallbackNode === null) {
      deadline = null
      console.timeEnd('async-log') // async-log: 7676.759033203125ms
      return
    }

    // 下一次继续调度
    requestIdleCallback(scheduleWork)

    // 当前执行刷新任务
    flushWork(console.log)
  }

  console.time('async-log')
  // 启动调度
  requestIdleCallback(scheduleWork)

}

可以看到在执行performAsync时,动画执行依旧比较流畅,当然相应所耗费时间还是增加的。

结论

浏览要干的活是一定的,你让他一下干完,时间很少,但是发生了明显的卡顿(他自己的活来不及干了)。让他空闲的时候执行,动画较为流畅,执行任务所需的时间也久了(给你干活的时间少了)

React团队同时引进了ConcurrentMode意为给任务增加不同的优先级,这样能够更好的调度,当然他采用“懒”策略,能不执行就不执行,等他要超时了,赶紧执行。保证了有大量间隙时间给用户交互,但是任务越积越多,到最后“一大笔帐”要算的时候,还是会出现卡顿。

文中如有欠妥的地方,欢迎指正。