「译」JavaScript 计时器之旅


JavaScript 计时器之旅

突击小测验: JavaScript 各类定时器之间的区别是什么?javascript

  • Promises
  • setTimeout
  • setInterval
  • setImmediate
  • requestAnimationFrame
  • requestIdleCallback

更具体地讲,若是你马上对这些计时器进行排序,知道他们触发的顺序是什么吗?css

若是不能,那你可能并不孤独。我已经写 JavaScript 和作编程许多年,曾经为一家浏览器厂商工做超过两年,直到最近,我才真正了解了这些计时器以及如何使用它们。html

在这篇文章中,我将高度概述这些定时器工做方式以及使用它们的时机,而且会一块儿介绍 Lodash 颇有用的 debounce()throttle() 函数。java

Promises 和 microtasks

让咱们先从这里开始,由于它大概是最简单的了。一个 Promise 回调也被称为 “microtask”,它以与 MutationObserver 回调相同的频率运行。若是 queueMicrotask() 没有被规范排除而且进入浏览器领域,它也会有一样的结果。node

我已经写过不少关于 promise 的文章。然而值得一提的是,Promise 有一个很容易被误解的地方是它们不会给浏览器留空闲的时间。那是由于处于异步回调队列中,可是并不意味着浏览器能够进行渲染,或者处理输入,或者作其余咱们但愿浏览器作的工做。react

举个例子,假设咱们有一个阻塞主线程1秒钟的函数:git

function block() {
  var start = Date.now()
  while (Date.now() - start < 1000) { /* wheee */ }
}
复制代码

若是咱们用一组 microtasks 来调用这个函数:github

for (var i = 0; i < 100; i++) {
  Promise.resolve().then(block)
}
复制代码

这将会阻塞浏览器100秒。这与下面的操做同样:web

for (var i = 0; i < 100; i++) {
  block()
}
复制代码

任何同步任务执行完成后,microtasks 会当即执行。在这二者之间没有空闲作其余工做。因此,若是想把一个运行时间较长的任务分解为 microtasks,是不会如你所愿的。npm

setTimeout 和 setInterval

它们是两兄弟:setTimeout 将任务排在 X 毫秒以后运行,而 setInterval 每隔 X 毫秒运行一次任务。

因为许多网站好比 confetti 处处乱用 setTimeout(0)。为了不阻塞浏览器主线程,浏览器必须为 setTimeout(/* ... */, 0) 添加缓解措施。

这就是crashmybrowser.com 中许多技巧再也不起做用的缘由,好比,在 setTimeout 中调用另外两个调用了更多 setTimeoutsetTimeout等等。我在 “Improving input responsiveness in Microsoft Edge” 中从边缘部分介绍了其中一些缓解方法。

宽泛地说,setTimeout(0) 不是真正的在0毫秒以后执行。一般会在4毫秒内执行。有时会在16毫秒内执行(当 Edge 在充电时会这样)。有时候还会被限制到1秒钟(例子:when running in a background tab)。这些是浏览器必须具有的能力,为了防止不受控制的网页占用 CPU 执行无用的 setTimeout

因此说,setTimeout 确实容许浏览器在回调函数被调用以前作一些工做(和 microtasks 不一样)。可是,若是你想在回调以前进行输入或是渲染操做,通常来讲 setTimeout 不是最好的选择,由于它只是偶尔容许在回调以前作其余操做。 如今,有更好的浏览器 API 能够更直接地挂到浏览器渲染系统中。

setImmediate

在继续介绍使用“更好的浏览器 API ”以前,这里有件事情值得一提。称为setImmediate 是由于缺乏一个更好的词语...很奇怪。若是在caniuse.com上查找,你会发现只有 Microsoft 浏览器支持它。可是它也在 node.js 中存在。这究竟是个什么东西?

setImmediate 最初是由微软提出来解决上述 setTimeout 的问题的。基本上,setTimeout 已经被滥用了,setImmediate(0) 实际上就是 setImmediate(0),而不是一个被限制在4毫秒的东西。你能够查看 some discussion about it from Jason Weber back in 2011

不幸的是,setImmediate 只被 IE 和 Edge 采用了。仍在使用的部分缘由是它在 IE 浏览器中做用很大,它容许输入事件好比键盘输入和鼠标点击“跳过队列”并在 setImmediate 回调以前执行,而 setTimeout 在 IE 中就没有这么大魔力。(Edge 最终解决了这个问题,详细说明在上一篇文章中)。

并且,setImmediate 存在于 Node 中这一事实意味着许多 “Node-polyfilled” 代码在浏览器中使用它,可是并不真正知道它在作什么。Node 中 process.nextTicksetImmediate的区别使人很困惑,甚至 Node 的官方文档都说名字应该交换。(然而为了这篇文章的初衷,我会把重心放在浏览器而不是 Node 上,由于我不是一个 Node 专家)。

最低原则:若是你知道你要作什么而且尝试优化 IE 的输入性能,就使用 setImmediate。若是不是,就不用麻烦了。(或者只在 Node 中使用)

requestAnimationFrame

如今,咱们有一个最重要的 setTimeout 替代品,一个真正挂在浏览器渲染循环中的定时器。顺便说一句,若是你不知道浏览器事件循环机制,我强烈推荐 Jake Archibald 的这个演讲

requestAnimationFrame 基本上是这样工做的:它虽然和 setTimeout 有点像,可是它会在浏览器下次重绘时调用,而非等待一些没法预测的时间(4毫秒,16毫秒,1秒等)。如今,像 Jake 在他的演讲中指出的同样,这里有一个小问题,在 Safari 、IE 和 Edge 18如下版本的浏览器中,他在样式/布局计算以后执行。可是让咱们忽略它,由于这不是一个很重要的细节。

我认为 requestAnimationFrame 的使用方式是这样的:不管何时,只要我知道我将要修改浏览器的样式或布局——举个例子,改变 CSS 属性或启动一个动画——我就会把它放在 requestAnimationFrame(这里缩写为 rAF)。这样确保了几件事情:

  1. 我不太可能打乱布局,由于全部的DOM的变化都在排队和协调。
  2. 个人代码会天然地去适应浏览器的性能特色。举个例子,若是这里有一个配置较低的设备正在试图渲染一些DOM元素,rAF 会天然地从一般的16.7毫秒(在60赫兹的屏幕上)时间间隔慢下来,所以,它不会像运行了大量 setTimeout 或 setInterval 的同样让设备崩溃。

这就是为何不依赖 CSS 转换或 keyframes 的动画库的缘由,好比 GreenSock or React Motion,一般会在 rAF 回调中更改。若是一个元素在 opacity: 0opacity: 1 之间进行动画转换,那么排队等待十亿次回调来对每一个可能的中间状态进行处理是没有意义的,包括 opacity: 0.0000001opacity: 0.9999999

相反,你最好只使用 rAF,让浏览器告诉你在给定的时间段能绘制多少帧,并为特定帧进行计算。这样,较慢的设备天然就会以慢的帧速率结束,较快的设备以快的帧速率结束,若是使用相似 setTimeout 这种独立于浏览器绘制速度的 API,上述状况都是不可能出现的。

requestIdleCallback

rAF 多是 toolkit 中最有用的定时器,可是requestIdleCallback 也一样值得一提。浏览器支持不是很好,可是有一个 工做很不错的polyfill(底层使用了 rAF)。

在不少状况下 rAF 相似于 requestIdleCallback。(从这开始缩写为 rIC

rAF 同样,rIC 会天然地适应浏览器的性能特征:若是设备过载,rIC 可能会延迟。rIC 的不一样之处在于它会在浏览器空闲状态触发,好比,当浏览器肯定它没有其余任务,microtasks 或输入事件要处理的时候,你就自由地作想作的工做。它也会给你一个 "deadline" 来追踪使用的预算值,这是个很不错的特性。

Dan Abramov 在2018 冰岛 JSConf 上有一个精彩讲话,在谈话中他展现了如何使用 rIC。在谈话中,有一个 webapp 在用户打字的每一次键盘输入的时候会调用 rIC,而后它会更新回调中的渲染状态。这很棒,由于一个快速打字的用户会致使 keydown/keyup 事件很是快地触发,可是你并不但愿为每一个按键都从新渲染页面。

另外一个很好的例子是 Twitter 或 MastoDon 上的“剩余字符计数”指示器。在 Pinafore 中,我使用 rIC 进行操做,由于我不真正关心指示符是否针对我每一次输入都从新渲染。若是我快速打字,最好优先考虑输入相应,这样才不会失去流畅感。

在 Pinafore 中,输入框下面的小提示条和“剩余字符”提示会随着输入而更新。

我注意到 rIC 在 Chrome 中有点瑕疵。在Firefox 中,每当我直觉的认为浏览器是空闲并准备运行一些代码的时候,它就会运行。(在 pollyfill 中也是这样。)不过在 Chrome 的安卓移动模式中,我注意到,每当我触摸滚动的时候,它就会将 rIC 延迟几秒钟,即便在我刚触摸完屏幕,浏览器也什么都不会作。(我怀疑我看到的问题是这个.)

更新:来自 Chrome 团队的 Alex Russell 通知我这是一个已知 bug,应该很快就修复!

不管如何,rIC 是另外一个很好地工具。我倾向于这样想:使用 rAF 来进行关键的渲染工做,使用 rIC 来进行非关键的渲染工做。

debounce 和 throttle

这里有两个非浏览器内置的方法,可是它们颇有用并值得了解。若是你不熟悉它们,这里有一个很棒的 CSS 技巧攻略

debounce 的标准用法是在 resize回调中。当用户调整浏览器窗口大小的时候,不必在每一个 resize 回调中更新布局,由于触发太频繁了。相反,你能够 debounce 几百毫秒,这会保证回调在用户在处理完窗口大小后触发。

throttle,另外一方面,是我使用得更多的方法。举个例子,scroll 事件是一个很棒的使用示例。再说一遍,对于每一个 scroll 回调都更新一遍视图状态是没有意义的,由于触发频率过高了(频率在不一样浏览器,不一样输入法之间是不一样的)。使用 throttle 能够规范这个行为,并确保它只在每 X 毫秒后触发。你能够调整 Lodash 的 throttle(或者 debounce)方法启动延迟的时机,在结束的时候或者不启动。

相反,我不会在滚动场景中使用 debounce,由于我不但愿 UI 仅在用户明确中止滚动后才更新。由于这可能会让用户苦恼和困惑,而且试图滚动继续更新 UI 状态(例如在无限滚动列表中)。

我在各类用户输入和一些定时安排的任务中会使用 throttle,好比 IndexedDB 清理。也许有一天它会内置到浏览器中。

结论

这是我对浏览器中各类定时器的快速了解以及如何使用它们。我可能漏掉了一些,由于这里有一些特殊的特性(postMessagelifecycle events,还有其余的吗?)。但但愿这至少能对我如何看待 JavaScript 中定时器有一个很好地概述。

相关文章
相关标签/搜索