- 原文地址:A tour of JavaScript timers on the web
- 原文做者:Nolan Lawson
- 譯文出自:阿里雲翻譯小組
- 譯文連接:github.com/dawn-teams/…
- 譯者:靈沼
- 校對者:也樹、靖鑫、眠雲
突擊小測驗: JavaScript 各類定時器之間的區別是什麼?javascript
更具體地講,若是你馬上對這些計時器進行排序,知道他們觸發的順序是什麼嗎?css
若是不能,那你可能並不孤獨。我已經寫 JavaScript 和作編程許多年,曾經爲一家瀏覽器廠商工做超過兩年,直到最近,我才真正瞭解了這些計時器以及如何使用它們。html
在這篇文章中,我將高度概述這些定時器工做方式以及使用它們的時機,而且會一塊兒介紹 Lodash 頗有用的 debounce()
和 throttle()
函數。java
讓咱們先從這裏開始,由於它大概是最簡單的了。一個 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 將任務排在 X 毫秒以後運行,而 setInterval 每隔 X 毫秒運行一次任務。
因爲許多網站好比 confetti 處處亂用 setTimeout(0)
。爲了不阻塞瀏覽器主線程,瀏覽器必須爲 setTimeout(/* ... */, 0)
添加緩解措施。
這就是crashmybrowser.com 中許多技巧再也不起做用的緣由,好比,在 setTimeout
中調用另外兩個調用了更多 setTimeout
的 setTimeout
等等。我在 「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 能夠更直接地掛到瀏覽器渲染系統中。
在繼續介紹使用「更好的瀏覽器 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.nextTick
和 setImmediate
的區別使人很困惑,甚至 Node 的官方文檔都說名字應該交換。(然而爲了這篇文章的初衷,我會把重心放在瀏覽器而不是 Node 上,由於我不是一個 Node 專家)。
最低原則:若是你知道你要作什麼而且嘗試優化 IE 的輸入性能,就使用 setImmediate
。若是不是,就不用麻煩了。(或者只在 Node 中使用)
如今,咱們有一個最重要的 setTimeout
替代品,一個真正掛在瀏覽器渲染循環中的定時器。順便說一句,若是你不知道瀏覽器事件循環機制,我強烈推薦 Jake Archibald 的這個演講。
requestAnimationFrame
基本上是這樣工做的:它雖然和 setTimeout
有點像,可是它會在瀏覽器下次重繪時調用,而非等待一些沒法預測的時間(4毫秒,16毫秒,1秒等)。如今,像 Jake 在他的演講中指出的同樣,這裏有一個小問題,在 Safari 、IE 和 Edge 18如下版本的瀏覽器中,他在樣式/佈局計算以後執行。可是讓咱們忽略它,由於這不是一個很重要的細節。
我認爲 requestAnimationFrame
的使用方式是這樣的:不管何時,只要我知道我將要修改瀏覽器的樣式或佈局——舉個例子,改變 CSS 屬性或啓動一個動畫——我就會把它放在 requestAnimationFrame
(這裏縮寫爲 rAF
)。這樣確保了幾件事情:
這就是爲何不依賴 CSS 轉換或 keyframes 的動畫庫的緣由,好比 GreenSock or React Motion,一般會在 rAF 回調中更改。若是一個元素在 opacity: 0
和 opacity: 1
之間進行動畫轉換,那麼排隊等待十億次回調來對每一個可能的中間狀態進行處理是沒有意義的,包括 opacity: 0.0000001
和 opacity: 0.9999999
。
相反,你最好只使用 rAF
,讓瀏覽器告訴你在給定的時間段能繪製多少幀,併爲特定幀進行計算。這樣,較慢的設備天然就會以慢的幀速率結束,較快的設備以快的幀速率結束,若是使用相似 setTimeout
這種獨立於瀏覽器繪製速度的 API,上述狀況都是不可能出現的。
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
來進行非關鍵的渲染工做。
這裏有兩個非瀏覽器內置的方法,可是它們頗有用並值得了解。若是你不熟悉它們,這裏有一個很棒的 CSS 技巧攻略
debounce
的標準用法是在 resize
回調中。當用戶調整瀏覽器窗口大小的時候,不必在每一個 resize
回調中更新佈局,由於觸發太頻繁了。相反,你能夠 debounce
幾百毫秒,這會保證回調在用戶在處理完窗口大小後觸發。
throttle
,另外一方面,是我使用得更多的方法。舉個例子,scroll
事件是一個很棒的使用示例。再說一遍,對於每一個 scroll
回調都更新一遍視圖狀態是沒有意義的,由於觸發頻率過高了(頻率在不一樣瀏覽器,不一樣輸入法之間是不一樣的)。使用 throttle
能夠規範這個行爲,並確保它只在每 X 毫秒後觸發。你能夠調整 Lodash 的 throttle
(或者 debounce
)方法啓動延遲的時機,在結束的時候或者不啓動。
相反,我不會在滾動場景中使用 debounce
,由於我不但願 UI 僅在用戶明確中止滾動後才更新。由於這可能會讓用戶苦惱和困惑,而且試圖滾動繼續更新 UI 狀態(例如在無限滾動列表中)。
我在各類用戶輸入和一些定時安排的任務中會使用 throttle
,好比 IndexedDB 清理。也許有一天它會內置到瀏覽器中。
這是我對瀏覽器中各類定時器的快速瞭解以及如何使用它們。我可能漏掉了一些,由於這裏有一些特殊的特性(postMessage
或 lifecycle events
,還有其餘的嗎?)。但但願這至少能對我如何看待 JavaScript 中定時器有一個很好地概述。