請抱著懷疑的態度使用 setTimeout

常常會聽聞「這支副程式 1 秒後檢查這個,然後再等 2 秒再做這些,再等 3 秒過後修改 state …預計 6 秒後狀態一定會被改變。」這類操作 callback 的「時間預期」,但是,真如你所想的如此嗎?

realdennis
6 min readSep 10, 2019
setTimeout callback hell
副標題所提到的例子

寫在文前

由於 setTimeout 的 callback hell 實在是太過刺眼,所以容許在下我將 setTimeout 做一個 Promise 的封裝。

const sleep = (sec) => 
new Promise(resolve => setTimeout(resolve), sec*1000);

Promise chain

sleep(1).then(()=>sleep(2)).then(()=>sleep(3)).then()

await

await sleep(1);
await sleep(2);
await sleep(3);

本文中的例子會由 await 來為各位做演示。

抱著「懷疑」的態度使用 setTimeout

副標題所提到的例子常見於處理動畫,有時我們希望動畫的開始、結束如我們所預期,比如:

// 第一個動畫要 1 秒
// 第一個動畫結束後 第二個動畫開始 要 2 秒
// 第二個動畫結束後 第三個動畫開始 要 3 秒
// 第三個動畫結束後 改變狀態

針對上面需求,我們的大腦可能會第一時間轉換成下列的程式碼:

animation1();
await sleep(1)
animation2();
await sleep(2);
animation3();
await sleep(3);
changeState(true)

(先讓我們無視 duration、delay 等等好用的東西)

這樣會造成什麼問題呢?動畫會有些許的「掉幀」行為,意思即兩個動畫開始與結束的時間卡不上,出現瞬間沒有動畫的狀況。

為什麼呢?

由於 setTimeout 會把回呼函數丟到 queue 裡頭,並不會在絕對的時間內拉出來執行,尤其在寫成 await 的形式後,我們常常會把這個 sleep 概念式連結到同步程式的 sleep 語法。

講這麼多,你證明給我看啊?

上面的道理大家都懂,這篇文章主要還是想做個實驗。

這個實驗中我們會用到 performance.now 這個好用的 API,並且在一系列的 timer 結束後,做一個時間差比較。

真的有誤差,沒騙你吧。

這張圖可以看出來,六秒完成的事情出現了將近 0.01 秒的誤差。這樣感覺好像也還好啊!感覺可要接受,哪有「掉幀」這麼誇張?

有興趣的人可以把這段程式碼,貼在「正在載入期間」的網站、或是富應用的網頁程式上,比如 Facebook ,你會發現偶爾會得出 7–9 秒的結果。

比如這樣:

程式碼沒變喔

很明顯我們的 「 6 秒程式」出現了不可控的味道。

原因在於 JavaScript 是一個單線程的應用,所以我們的 callback 實際上會依序排隊在 callback queue 裡頭。

但是,你也知道,整個網頁不是只有你一個人在 callback 啊,別人可能也會塞一堆 callback 進來,或是出現一個 callback 他執行的爆幹久(尤其是操作DOM節點),輪到你的 callback 被拉出來的時候早就不知道過多久了。

言下之意:

你的 setTimeout 會比你想象的還要撈賽,尤其你的副程式如果是在頁面加載時、頁面高互動的話…

你也許會想:這篇文章在販賣焦慮,沒這麼嚴重吧?

你說的或許是。

在 「6秒程式」的例子中,讓 setTimeout 失準的 case 聽起來相當的極端,比如在平常安安靜靜的 Medium 裡頭按下 F12 並且貼上這段程式碼誤差不過 0.01 秒,甚至不到。

這點誤差有什麼好撰文的呢?

好,就是這個點,讓我們來做一個小實驗。

我們來想象一隻程式,一隻我們認為會在「3 秒內完成」的副程式吧:

30次進入queue / 誤差 0.06 sec

當進入 queue 的次數上升,queue裡頭會有越多的排隊…

150次進入queue / 誤差 0.3 sec

開始逐漸母湯

3000次進入queue / 誤差 11sec

哇!這個誤差的量級突然大暴增了,主要是 setTimeout 其實最小只能接受一個值,儘管你設定 0 ,它仍然會等不會馬上去執行。

這也是我第二個例子要舉 150次 queue / 20ms 的 timeout 的原因。

結論

從上述實驗中,我們可以明白幾件事:

  • setTimeout 不可信
  • JavaScript 是 single thread 在大型網站中 setTimeout 更是延遲
  • 過小的 Timeout 讓 setTimeout 失去意義
  • 幾秒後幹嘛…然後幾秒後幹嘛換一個寫法

那麼我們可以怎麼解決呢?

  1. 直接不相信 setTimeout 會依照時間走
  2. 盡量不要用短時間 setTimeout

如何解決動畫順序問題?

  • 使用 CSS 中對應的 duration / delay 之 property

如何解決「這個 function 不確定要多久,但最多 2 秒…所以只能等 2 秒後再做下一個的程式…」?

這裡給兩個思路:

  1. 使用 customEvent 的方式,在function 執行完畢後 dispatchEvent 出來,而後續要做的事情採 subscription flow 的方式撰寫副程式。
  2. 使用 Promise 封裝上頭不可描述的非同步 function ,並且透過 then、catch / try await … catch 的 flow 去撰寫。

--

--

No responses yet