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