在 JS 中如何调度后台任务?

关于 JavaScript,就算什么都不记得了,也请牢记这一点:它是阻塞式的。

想象一下,你的浏览器是靠一个神奇的小精灵来运行的。所有的事情都是这一个小精灵来干的,不管是渲染 HTML、响应菜单的命令、在屏幕上绘图、处理鼠标点击,还是执行一个 JavaScript 函数。和我们大多数人一样,小精灵同时只能做一件事。如果我们把好多事都丢给小精灵,它们会被排到一个待处理列表中,按照顺序依次执行。

当小精灵碰到一个 script 标签,或者必须运行一个 JavaScript 函数的时候,所有事情都得停下来。首先得下载代码(如果需要的话),然后立即执行这些脚本,再然后才能继续执行后面的事件或者渲染操作。这个过程是必须的,因为你的脚本可能执行任何操作:加载后续代码、移除所有 DOM 元素、跳转到其他的 URL 等等。就算有两个或者更多的小精灵,在第一个小精灵处理你的代码的时候,剩下那些也得停下来等着。这就是所谓的阻塞式。在长时间运行脚本的时候,正式这种机制导致了浏览器会失去响应。

你经常希望你的 JavaScript 代码能够尽快执行,因为这些代码会初始化组件和事件处理函数。然而,也有一些不那么重要的后台任务,它们并不会直接影响用户体验,例如:

  • 记录分析数据

  • 把数据发送到社交网络(或者添加 57 个“分享”按钮)

  • 内容预读取

  • HTML 的预处理或者预渲染

这些任务在时间上并不是那么重要,不过为了让页面保持响应,在用户滚动页面或者与内容进行交互的时候,它们就不应该运行了。

选择之一是使用 Web Worker,它可以在一个独立的线程中并行运行。对于内容预读取和预处理工作而言,这是个不错的选择,不过在 Web Worker 中,你无法直接访问或者更新 DOM。当然你可以在你自己的代码中避免这一点,但你无法保证永远不会依赖第三方代码,比如 Google Analytics。

另一种可能的方式是使用 setTimeout,比如:setTimeout(doSomething, 1);。浏览器会在其它立即执行的任务完成后,再去执行 doSomething() 函数。实际上,这种方式是把它放到了待处理列表的最后面。不幸的是,不管是否需要,这个函数最终都会被调用。

requestIdleCallback

requestIdleCallback 是一个全新的 API,它可以在浏览器空闲的时候去调度那些不那么重要的后台任务。与之类似的还有 requestAnimationFrame,可以通过调用一个函数在下一次重绘之前更新动画。更多关于 requestAnimationFrame 的内容请见:使用 requestAnimationFrame 实现简单动画。

我们可以通过如下方法检测浏览器是否支持 requestIdleCallback:


if ('requestIdleCallback' in window) {

  // requestIdleCallback supported

  requestIdleCallback(backgroundTask);

}

else {

  // no support - do something else

  setTimeout(backgroundTask1, 1);

  setTimeout(backgroundTask2, 1);

  setTimeout(backgroundTask3, 1);

}

也可以指定一个 options 对象作为参数,其中包含超时时间(以毫秒为单位),例如:


requestIdleCallback(backgroundTask, { timeout: 3000; });

这种方式可以确保你的函数在前 3 秒之内被调用,不论浏览器是否空闲。

requestIdleCallback 只会调用你的函数一次,并传递一个 deadline 对象,该对象内包含如下属性:

  • didTimeout — 如果上文提到的超时事件被触发了,该属性为 true

  • timeRemaining() — 该函数会返回执行任务所剩余的毫秒数

timeRemaining() 会给任务运行分配不超过 50 毫秒的时间。如果任务执行超过这个时间的话,任务并不会被强行终止,不过推荐的做法是,此时你应该再次调用 requestIdleCallback 来调度后续的处理流程。

让我们创建一个简单的示例,按照顺序执行多个任务。这些任务存储在一个数组中,该数组会在函数内使用:


// array of functions to run

var task = [

background1,

background2,

background3

];

if ('requestIdleCallback' in window) {

  // requestIdleCallback supported

  requestIdleCallback(backgroundTask);

}

else {

  // no support - run all tasks soon

  while (task.length) {

   setTimeout(task.shift(), 1);

  }

}

// requestIdleCallback callback function

function backgroundTask(deadline) {

  // run next task if possible

  while (deadline.timeRemaining() > 0 && task.length > 0) {

   task.shift()();

  }

  // schedule further tasks if necessary

  if (task.length > 0) {

    requestIdleCallback(backgroundTask);

  }

}

是否有什么事情是不应该放在 requestIdleCallback 中执行的?

Paul Lewis 在他的博客中进行了说明,在 requestIdleCallback 中执行的操作应该是小片段的形式,不应该执行那些运行时间无法预测的任务(比如处理 DOM 更适合使用 requestAnimationFrame 回调)。你同样需要当心 Promise 对象的 resolve 或 reject,因为在空闲回调结束后,即使已经没有剩余时间,Promise 的回调也会立即执行。

requestIdleCallback 的浏览器支持情况

requestIdleCallback 是一个试验性质的特性,规范依然有可能会发生变化,所以如果你遇到了 API 变动的情况请不要吃惊。Chrome 47 支持该特性,也就是说应该在 2015 年结束之前。Opera 也应该很快就会支持该特性。微软和 Mozilla 也在考虑该 API,看起来问题不大。苹果公司一如既往地对此不置可否。如果你现在就要尝尝鲜,你的最佳选择是使用 Chrome Canary(最新版本的 Chrome,没有经过全面测试,但是其中包含了最新的特性)。(译注:在本文翻译时 Chrome 47 已经发布,并支持该特性)

之前提到的 Paul Lewis 创建了一个简单的 requestIdleCallback 替代品。它会实现同样功能的 API,不过它并不是完整的,因为没有模拟浏览器的空闲行为检测。它是靠 setTimeout 来完成功能的,正如上面的例子一样。如果你想要使用该 API,又不想做对象检测和代码更新的话,这也是个不错的选择。

尽管到目前为止支持还比较有限,requestIdleCallback 还是个有趣的东西,可以帮助你最大限度的优化网页性能。不过你是怎么想的呢?欢迎在本文下方发表你的评论。

上一篇:别用setInterval_成都兰州西安


下一篇:Promise介绍和基本使用