【第1419期】JavaScript 計時器之旅

尋夢新聞LINE@每日推播熱門推薦文章,趣聞不漏接❤️

加入LINE好友

前言

最近在體驗不同的東西,有點意思。今日早讀文章由阿里雲中台體驗技術部@靈沼翻譯授權分享。

正文從這開始~~

突擊小測驗: Java 各種定時器之間的區別是什麼?

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

更具體地講,如果你立刻對這些計時器進行排序,知道他們觸發的順序是什麼嗎?

如果不能,那你可能並不孤獨。我已經寫 Java 和做編程許多年,曾經為一家瀏覽器廠商工作超過兩年,直到最近,我才真正了解了這些計時器以及如何使用它們。

在這篇文章中,我將高度概述這些定時器工作方式以及使用它們的時機,並且會一起介紹 Lodash 很有用的 debounce() 和 throttle() 函數。

Promises 和 microtasks

讓我們先從這里開始,因為它大概是最簡單的了。一個 Promise 回調也被稱為 「microtask」,它以與 MutationObserver 回調相同的頻率運行。如果 queueMicrotask() 沒有被規範排除並且進入瀏覽器領域,它也會有同樣的結果。

我已經寫過很多關於 promise 的文章。然而值得一提的是,Promise 有一個很容易被誤解的地方是它們不會給瀏覽器留空閒的時間。那是因為處於異步回調隊列中,但是並不意味著瀏覽器可以進行渲染,或者處理輸入,或者做其他我們希望瀏覽器做的工作。

舉個例子,假設我們有一個阻塞主線程1秒鐘的函數:

functionblock(){

varstart =Date.now()

while(Date.now()start <1000){/* wheee */}

}

如果我們用一組 microtasks 來調用這個函數:

for(vari =0;i <100;i++){

Promise.resolve().then(block)

}

這將會阻塞瀏覽器100秒。這與下面的操作一樣:

for(vari =0;i <100;i++){

block()

}

任何同步任務執行完成後,microtasks 會立即執行。在這兩者之間沒有空閒做其他工作。所以,如果想把一個運行時間較長的任務分解為 microtasks,是不會如你所願的。

setTimeout 和 setInterval

它們是兩兄弟: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 可以更直接地掛到瀏覽器渲染系統中。

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.nextTick 和 setImmediate的區別令人很困惑,甚至 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 的動畫庫的原因,比如 GreenSockor React Motion,通常會在 rAF 回調中更改。如果一個元素在 opacity: 0 和 opacity: 1 之間進行動畫轉換,那麼排隊等待十億次回調來對每個可能的中間狀態進行處理是沒有意義的,包括 opacity: 0.0000001 和 opacity: 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 進行操作,因為我不真正關心指示符是否針對我每一次輸入都重新渲染。如果我快速打字,最好優先考慮輸入相應,這樣才不會失去流暢感。

【第1419期】JavaScript 計時器之旅

在 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 清理。也許有一天它會內置到瀏覽器中。

結論

這是我對瀏覽器中各種定時器的快速了解以及如何使用它們。我可能漏掉了一些,因為這里有一些特殊的特性(postMessage或 lifecycle events,還有其他的嗎?)。但希望這至少能對我如何看待 Java 中定時器有一個很好地概述。

關於本文

譯者:@靈沼

譯文:

https://zhuanlan.zhihu.com/p/46739421

作者:@Nolan Lawson

原文:

https://nolanlawson.com/2018/09/01/a-tour-of-java-timers-on-the-web/

【第1201期】Node 定時器詳解

【活動】第二屆Vue.js開發者大會,11月24號杭州見!

Ta還分享過

【第1389期】一起探討 Java 的對象

About 尋夢園
尋夢園是台灣最大的聊天室及交友社群網站。 致力於發展能夠讓會員們彼此互動、盡情分享自我的平台。 擁有數百間不同的聊天室 ,讓您隨時隨地都能找到志同道合的好友!