本文 AI 產出,尚未審核

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 進行動畫更新。

具體的最佳實踐

  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) 中,切換頁面時清除不再需要的計時器,避免背景持續執行。

總結

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

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