為什么你的 setTimeout 并不像你以為的那樣工作
在 JavaScript 世界中,setTimeout
常常被視為「延遲執(zhí)行」的萬能工具。但真正掌握它的行為細(xì)節(jié),并不如想象中簡(jiǎn)單。許多開發(fā)者明明寫下了 setTimeout(fn, 1000)
,卻驚訝于它“沒準(zhǔn)時(shí)”、“被跳過”甚至“完全不執(zhí)行”。
? 基礎(chǔ)誤解:setTimeout(fn, 1000)
≠ 準(zhǔn)時(shí)執(zhí)行
setTimeout
并不保證回調(diào)函數(shù)會(huì)在指定的毫秒數(shù)后立即執(zhí)行。
它的真實(shí)含義是:
“將
fn
在至少delay
毫秒后,加入任務(wù)隊(duì)列(callback queue),并等待主線程空閑時(shí)執(zhí)行?!?/span>
例子:看似直覺的錯(cuò)覺
console.log("Start");
setTimeout(() => {
console.log("Timeout!");
}, 0);
console.log("End");
輸出:
Start
End
Timeout!
原因很簡(jiǎn)單:即使 delay
設(shè)置為 0
,任務(wù)也只能在當(dāng)前執(zhí)行棧(call stack)清空后被調(diào)度執(zhí)行。
?? 主線程阻塞會(huì)直接影響定時(shí)器
考慮下面的代碼:
console.log("Start");
setTimeout(() => {
console.log("Timeout!");
}, 1000);
const now = Date.now();
while (Date.now() - now < 3000) {
// 模擬阻塞主線程
}
console.log("End");
盡管延遲設(shè)置為 1s
,回調(diào)實(shí)際要等 超過 3 秒 后才會(huì)執(zhí)行。這就是 JavaScript 的單線程模型導(dǎo)致的“延遲漂移”現(xiàn)象。
?? 重復(fù)使用 setTimeout
的隱藏陷阱
部分開發(fā)者使用遞歸實(shí)現(xiàn)定時(shí)器:
function tick() {
console.log("Tick");
setTimeout(tick, 1000);
}
tick();
雖然看起來每秒執(zhí)行一次,但如果 tick
中包含耗時(shí)操作,下一次調(diào)度就會(huì)被推遲。這種方式并不適合對(duì)時(shí)間敏感的場(chǎng)景。
更推薦的做法:
- 需要精確間隔: 使用
setInterval
(需注意累積誤差) - 需要?jiǎng)赢嬃鲿承裕?/span> 使用
requestAnimationFrame
或基于時(shí)間差的計(jì)算
“定時(shí)器根本沒觸發(fā)” 的常見原因
當(dāng) setTimeout
根本沒執(zhí)行時(shí),可能不是代碼寫錯(cuò),而是以下情況導(dǎo)致:
- React/Vue 等框架中組件卸載導(dǎo)致定時(shí)器被清除
- 主動(dòng)或間接調(diào)用了
clearTimeout
- 瀏覽器處于后臺(tái)標(biāo)簽頁,觸發(fā)了定時(shí)器節(jié)流
- 運(yùn)行環(huán)境并不是真正的瀏覽器(如 Web Worker、Node.js 等)
Chrome、Safari、Firefox 等現(xiàn)代瀏覽器均會(huì)對(duì)非活動(dòng)標(biāo)簽頁的定時(shí)器進(jìn)行強(qiáng)制降頻或延遲,有時(shí)甚至高達(dá) 60 秒。
經(jīng)典 Closure 閉包陷阱
for (var i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}
預(yù)期輸出: 0, 1, 2, 3, 4實(shí)際輸出: 5, 5, 5, 5, 5
原因:var
是函數(shù)作用域,循環(huán)結(jié)束后 i
的值已經(jīng)是 5,所有回調(diào)共享同一個(gè)變量引用。
正確寫法:
使用 let
(塊級(jí)作用域):
for (let i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}
或使用 IIFE 包裝作用域:
for (var i = 0; i < 5; i++) {
(function(i) {
setTimeout(() => {
console.log(i);
}, 1000);
})(i);
}
總結(jié):關(guān)于 setTimeout
的關(guān)鍵認(rèn)知
規(guī)則 | 描述 |
非精確性 |
是最早何時(shí)加入隊(duì)列,不是保證準(zhǔn)時(shí)執(zhí)行 |
單線程限制 | 若主線程忙,定時(shí)器就會(huì)等待,導(dǎo)致延遲漂移 |
閉包陷阱 | 循環(huán)中使用 |
瀏覽器節(jié)流 | 后臺(tái)標(biāo)簽頁可能觸發(fā)延遲機(jī)制,長(zhǎng)時(shí)間不執(zhí)行 |
非阻塞調(diào)度 |
是調(diào)度機(jī)制,不是強(qiáng)制“倒計(jì)時(shí)觸發(fā)器” |