「譯」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 中定時器有一個很好地概述。

相關文章
相關標籤/搜索