JavaScript 事件循環:setTimeout 與 setInterval 完全攻略
簡介
在 JavaScript 的執行環境(瀏覽器或 Node.js)中,事件循環(Event Loop) 負責協調同步程式碼與非同步任務的執行順序。setTimeout 與 setInterval 是最常見的兩個計時 API,幾乎所有的前端互動、倒數計時、輪詢請求、以及簡易的排程,都離不開它們。
了解這兩個函式的運作機制,不只是能寫出正確的非同步程式碼,更能避免 計時器漂移、記憶體洩漏 等常見陷阱,提升應用效能與可維護性。本文將從概念、實作範例、常見問題到最佳實踐,完整說明 setTimeout / setInterval 在事件循環中的角色,讓初學者到中階開發者都能快速上手並在實務上正確運用。
核心概念
1. setTimeout:一次性的延遲執行
setTimeout(callback, delay, …args) 會在 至少 delay 毫秒 後,把 callback 放入 Task Queue,等到主執行緒空閒時才會被事件循環取出執行。
delay為 0 時,仍會等到目前的同步程式碼全部跑完才執行(稱為 macro‑task)。callback內的this依照呼叫方式決定,若使用箭頭函式則會保留外層的this。
範例 1:基本用法
console.log('A'); // 同步執行
setTimeout(() => {
console.log('B'); // 先放入 task queue,等事件循環執行
}, 0);
console.log('C'); // 同步執行
// 輸出順序:A C B
重點:即使
delay為 0,B仍會在C之後印出,因為它被排入 macro‑task。
2. setInterval:重複執行的計時器
setInterval(callback, interval, …args) 會每隔 interval 毫秒 把 callback 放入 Task Queue。若前一次的回呼尚未完成,下一次仍會排入,可能造成 重疊執行(timer drift)。
範例 2:簡易倒數計時
let count = 5;
const timerId = setInterval(() => {
console.log(`倒數 ${count--}`);
if (count < 0) clearInterval(timerId); // 結束計時器
}, 1000);
// 每秒印出 5 → 4 → 3 → 2 → 1 → 0,最後停止
3. 取得與清除計時器 ID
setTimeout 與 setInterval 皆回傳一個 計時器 ID(在瀏覽器是數字,在 Node.js 是 Timeout 物件),可用 clearTimeout(id) 或 clearInterval(id) 取消。
範例 3:動態取消
const id = setTimeout(() => {
console.log('永遠不會出現');
}, 5000);
// 2 秒後取消
setTimeout(() => {
clearTimeout(id);
console.log('計時器已取消');
}, 2000);
4. 計時器精準度與漂移
- 最小延遲:瀏覽器對
setTimeout、setInterval的下限通常是 4ms(在非活躍分頁更高)。 - 漂移:
setInterval的間隔是 排程 時間,而非「實際執行」時間。若回呼執行耗時較長,下一次排程會被推遲,產生漂移。
範例 4:觀察漂移
let start = Date.now();
let i = 0;
const int = setInterval(() => {
const now = Date.now();
console.log(`第 ${++i} 次,間隔 ${now - start} ms`);
start = now;
if (i === 5) clearInterval(int);
}, 1000);
// 若回呼內有阻塞操作,間隔會大於 1000ms
5. 用 Promise 包裝計時器:await 版的 sleep
在 async/await 流程中,我們常需要「暫停」一段時間,這時可以把 setTimeout 包成 Promise。
範例 5:sleep 函式
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function demo() {
console.log('開始');
await sleep(2000); // 暫停 2 秒
console.log('2 秒後');
}
demo();
常見陷阱與最佳實踐
| 陷阱 | 說明 | 建議的最佳實踐 |
|---|---|---|
| 計時器漂移 | setInterval 可能因前一次回呼執行時間過長而產生累積誤差。 |
使用 遞迴 setTimeout,每次根據實際執行時間重新計算間隔。 |
| 閉包捕獲 | 在迴圈內使用 var 宣告的計時器,回呼會抓到最後一次的變數值。 |
使用 let 或立即執行函式 (IIFE) 產生區塊作用域。 |
this 失效 |
傳統函式作為回呼時,this 會指向全域 (或 undefined in strict mode)。 |
使用 箭頭函式 或 callback.bind(this)。 |
| 忘記清除計時器 | 離開頁面或組件未銷毀時仍持續執行,造成記憶體洩漏。 | 在 componentWillUnmount、useEffect 的 cleanup 或類似機制中 clearTimeout / clearInterval。 |
過度使用 setInterval |
用於 UI 動畫時會與螢幕刷新不同步,造成卡頓。 | 改用 requestAnimationFrame 進行動畫更新。 |
具體的最佳實踐
永遠保存計時器 ID:使用
const timerId = setTimeout(...);,避免意外重寫。在需要精準節奏時使用遞迴
setTimeout:function tick() { console.log('tick'); setTimeout(tick, 1000); // 每次結束後再排程 } tick();避免長時間阻塞:回呼內不應執行大量同步運算,改用 Web Worker 或拆成多個小任務。
在測試環境中使用偽時間:如 Jest 的
jest.useFakeTimers(),方便驗證計時器行為。
實際應用場景
防抖 (Debounce) 與節流 (Throttle)
- 防抖:使用
setTimeout在使用者停止輸入後才觸發搜尋 API。 - 節流:使用
setInterval或遞迴setTimeout限制滑動事件的觸發頻率。
- 防抖:使用
輪詢 (Polling) API
- 定時以
fetch向伺服器取得最新資料,若回傳成功則clearInterval。
- 定時以
UI 動畫與倒數計時
- 製作簡易的倒數計時器、廣告輪播圖、或是提示訊息的自動關閉。
超時機制 (Timeout)
- 為 AJAX 請求設定最大等待時間,超過即 abort 請求並提示使用者。
節能與資源回收
- 在單頁應用 (SPA) 中,切換頁面時清除不再需要的計時器,避免背景持續執行。
總結
setTimeout 與 setInterval 是 JavaScript 事件循環中最基礎且最強大的非同步工具。掌握它們的 排程機制、計時器 ID 的管理、以及與事件循環的互動,不僅能寫出正確的延遲與重複執行程式碼,更能避免計時器漂移、記憶體洩漏等常見問題。
透過本文提供的 實作範例、陷阱解析與最佳實踐,你可以在日常開發中安全地運用計時器,從防抖搜尋、輪詢資料,到 UI 動畫與超時控制,都能得心應手。最後,別忘了在需要更精準或與畫面同步的情況下,評估使用 requestAnimationFrame、Web Worker 或 Promise 包裝的計時器,以達到最佳效能與使用者體驗。祝你在 JavaScript 的非同步世界裡玩得開心、寫得順手!