本文 AI 產出,尚未審核

JavaScript 事件循環:setTimeoutsetInterval 完全攻略


簡介

在 JavaScript 的執行環境(瀏覽器或 Node.js)中,事件循環(Event Loop) 負責協調同步程式碼與非同步任務的執行順序。setTimeoutsetInterval 是最常見的兩個計時 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

setTimeoutsetInterval 皆回傳一個 計時器 ID(在瀏覽器是數字,在 Node.js 是 Timeout 物件),可用 clearTimeout(id)clearInterval(id) 取消。

範例 3:動態取消

const id = setTimeout(() => {
  console.log('永遠不會出現');
}, 5000);

// 2 秒後取消
setTimeout(() => {
  clearTimeout(id);
  console.log('計時器已取消');
}, 2000);

4. 計時器精準度與漂移

  • 最小延遲:瀏覽器對 setTimeoutsetInterval 的下限通常是 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)
忘記清除計時器 離開頁面或組件未銷毀時仍持續執行,造成記憶體洩漏。 componentWillUnmountuseEffect 的 cleanup 或類似機制中 clearTimeout / clearInterval
過度使用 setInterval 用於 UI 動畫時會與螢幕刷新不同步,造成卡頓。 改用 requestAnimationFrame 進行動畫更新。

具體的最佳實踐

  1. 永遠保存計時器 ID:使用 const timerId = setTimeout(...);,避免意外重寫。

  2. 在需要精準節奏時使用遞迴 setTimeout

    function tick() {
      console.log('tick');
      setTimeout(tick, 1000); // 每次結束後再排程
    }
    tick();
    
  3. 避免長時間阻塞:回呼內不應執行大量同步運算,改用 Web Worker 或拆成多個小任務。

  4. 在測試環境中使用偽時間:如 Jest 的 jest.useFakeTimers(),方便驗證計時器行為。


實際應用場景

  1. 防抖 (Debounce) 與節流 (Throttle)

    • 防抖:使用 setTimeout 在使用者停止輸入後才觸發搜尋 API。
    • 節流:使用 setInterval 或遞迴 setTimeout 限制滑動事件的觸發頻率。
  2. 輪詢 (Polling) API

    • 定時以 fetch 向伺服器取得最新資料,若回傳成功則 clearInterval
  3. UI 動畫與倒數計時

    • 製作簡易的倒數計時器、廣告輪播圖、或是提示訊息的自動關閉。
  4. 超時機制 (Timeout)

    • 為 AJAX 請求設定最大等待時間,超過即 abort 請求並提示使用者。
  5. 節能與資源回收

    • 在單頁應用 (SPA) 中,切換頁面時清除不再需要的計時器,避免背景持續執行。

總結

setTimeoutsetInterval 是 JavaScript 事件循環中最基礎且最強大的非同步工具。掌握它們的 排程機制、計時器 ID 的管理、以及與事件循環的互動,不僅能寫出正確的延遲與重複執行程式碼,更能避免計時器漂移、記憶體洩漏等常見問題。

透過本文提供的 實作範例、陷阱解析與最佳實踐,你可以在日常開發中安全地運用計時器,從防抖搜尋、輪詢資料,到 UI 動畫與超時控制,都能得心應手。最後,別忘了在需要更精準或與畫面同步的情況下,評估使用 requestAnimationFrame、Web Worker 或 Promise 包裝的計時器,以達到最佳效能與使用者體驗。祝你在 JavaScript 的非同步世界裡玩得開心、寫得順手!