請抱著懷疑的態度使用 setTimeout
常常會聽聞「這支副程式 1 秒後檢查這個,然後再等 2 秒再做這些,再等 3 秒過後修改 state …預計 6 秒後狀態一定會被改變。」這類操作 callback 的「時間預期」,但是,真如你所想的如此嗎?
寫在文前
由於 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 秒內完成」的副程式吧:
當進入 queue 的次數上升,queue裡頭會有越多的排隊…
開始逐漸母湯
哇!這個誤差的量級突然大暴增了,主要是 setTimeout 其實最小只能接受一個值,儘管你設定 0 ,它仍然會等不會馬上去執行。
這也是我第二個例子要舉 150次 queue / 20ms 的 timeout 的原因。
結論
從上述實驗中,我們可以明白幾件事:
- setTimeout 不可信
- JavaScript 是 single thread 在大型網站中 setTimeout 更是延遲
- 過小的 Timeout 讓 setTimeout 失去意義
- 幾秒後幹嘛…然後幾秒後幹嘛換一個寫法
那麼我們可以怎麼解決呢?
- 直接不相信 setTimeout 會依照時間走
- 盡量不要用短時間 setTimeout
如何解決動畫順序問題?
- 使用 CSS 中對應的
duration
/delay
之 property
如何解決「這個 function 不確定要多久,但最多 2 秒…所以只能等 2 秒後再做下一個的程式…」?
這裡給兩個思路:
- 使用
customEvent
的方式,在function 執行完畢後dispatchEvent
出來,而後續要做的事情採 subscription flow 的方式撰寫副程式。 - 使用 Promise 封裝上頭不可描述的非同步 function ,並且透過 then、catch / try await … catch 的 flow 去撰寫。