使用 requestIdleCallback 实现协作式调度

TL;DR: requestIdleCallback 支持已在 Firefox Nightly 中实现,计划在 Firefox 52 中发布。

构建交互式网站最混乱的地方在于:主线程与 UI 线程是同一个。渲染页面和响应用户操作与计算、网络活动以及 DOM 操作存在竞争关系。其中一些操作可以通过 使用 Worker 安全地转移到其他线程,并且相对容易,但只有主线程可以更改 DOM 和许多其他 Web 平台功能。历史上,脚本没有办法与用户交互和页面渲染“和谐共处”,导致帧速率下降和输入滞后。

显然,如果情况仍然如此,我不会写这篇文章!

如果您必须在主线程上执行任务(更改 DOM 或与仅在主线程上运行的 Web API 交互),您现在可以请求浏览器为您提供一个可以安全执行此操作的时间窗口!以前,开发人员使用 setTimeout 让浏览器在周期性操作之间喘口气。这最多只会将任务推迟到事件循环的下一轮。并且它仍然会导致卡顿。

requestIdleCallback 应运而生。从表面上看,它的基本用法类似于 setTimeout,甚至更类似于 setImmediate

requestIdleCallback(sporadicScriptAction);

但是,浏览器有更多自由来满足您的请求。而不是等待特定的毫秒数或直到事件循环的下一个立即传递,requestIdleCallback 允许浏览器等待直到它识别到一个 空闲期。空闲期可能是两个帧之间绘制时仅有的几毫秒。

请记住,流畅的 60fps 动画在两帧之间只有 16 毫秒,其中大部分可能需要浏览器。所以我们说的是只有几毫秒!另外,如果没有任何动画或其他视觉变化正在发生,浏览器可以选择允许最多 50 毫秒。如果用户与页面交互,视觉反馈在 100 毫秒内完成会让人感觉“瞬间”。有了最多 50 毫秒的空闲时间,浏览器有充足的时间来优雅地处理任何传入的用户事件。

因此,您的回调可能会得到一个 1ms-10ms 的时间窗口来执行操作,也可能得到一个轻松悠闲的 50ms!您的代码如何知道?这就是 requestIdleCallback 与其祖先不同的部分。当您的回调被调用时,它会收到有关它可以执行操作的时间的信息。

// we know this action can take longer than 16ms,
// so let's be safe and only do it when we have the time.
function sporadicScriptAction (timing) {
  if (timing.timeRemaining() > 20) {
    // update DOM or what have you
  } else {
    // request another idle period or simply do nothing
  }
}

您的回调将传递一个 IdleDeadline 对象,该对象包含一个 timeRemaining 方法。timeRemaining() 将提供一个关于空闲期剩余时间的实时估计,允许脚本确定需要完成多少工作,或者是否需要完成工作。

如果我 *必须* 做事情怎么办?

协作式多任务处理是各方之间的谈判,为了成功,双方(浏览器和脚本)都需要做出让步。如果您的主线程操作必须在特定时间范围内完成(UI 更新或其他时间敏感的操作),脚本可以请求一个 timeout,在此之后回调 *必须* 运行。

// Call me when you have time, but wait no longer than 1000ms
requestIdleCallback(neededUIUpdate, { timeout: 1000 });

如果在请求的超时之前出现了空闲期,操作将如之前一样进行。但是,如果请求达到提供的超时时间,并且没有可用的空闲期,回调将无论如何运行。可以通过检查 IdleDeadline 对象的 didTimeout 属性来检测这一点。

function sporadicScriptAction (timing) {
  // will be 0, because we reached the timeout condition
  if (timing.timeRemaining() > 20) {

  } else if (timing.didTimeout) { // will be true

  }
}

不用了(取消调用)

与所有已安排的回调机制(setTimeoutsetImmediatesetInterval)和 requestAnimationFrame 一样,requestIdleCallback 返回一个句柄,可用于取消已安排的函数调用。

var candyTime = requestIdleCallback(goTrickOrTreating);

// it's November.
cancelIdleCallback(candyTime);

更协调的 Web

就像 requestAnimationFrame 为我们提供了与浏览器绘制协调的工具一样,requestIdleCallback 提供了一种与浏览器整体工作计划进行合作的方法。如果操作正确,用户甚至不会注意到您正在执行工作,他们只会感受到更流畅、更响应的网站。我们无法回到过去,在 JS 中将 UI 线程与主线程分离,但有了合适的工具、关注点分离和计划,我们仍然可以构建出色的交互式体验。


3 条评论

  1. Ashish Chaudhary

    我担心这会导致应用程序中的状态不一致,因为它不能保证函数会在回调队列中的某个事件之前或之后立即执行。

    例如,它无法保证函数会在点击处理程序执行之前执行。

    2016 年 11 月 9 日 下午 10:23

    1. Potch

      如果您有一个需要在特定时间内调用的函数,您仍然应该使用其他调度函数来确保排序正确。

      2016 年 11 月 14 日 上午 09:42

  2. Edwin Martin

    全局定时函数的数量有点失控了。

    将尚未标准化的 setImmediate 和 requestIdleCallback 合并到一个函数中会很好。只需将其称为 requestCallback,并使其也适用于立即回调,例如通过将超时设置为 0 或字符串“immediate”。然后删除命名不好的 setImmediate 函数。

    2016 年 11 月 11 日 上午 06:15

本文评论已关闭。