Node.js Timers:全面精通定時器與執行順序

在 JavaScript 中,我們經常需要延遲執行程式碼或定期執行某個任務。Node.js 除了提供標準的 setTimeoutsetInterval 外,還提供了更貼近事件迴圈底層的 setImmediate,以及強大的 Promise 版本定時器。

setTimeout:一次性延遲執行

setTimeout(callback, delay) 用於在指定的毫秒數之後執行一次回呼。

  • 核心認知delay 參數並非「精確的執行時間」,而是「最快可執行的時間」。如果當時事件迴圈的主執行緒正在處理繁重的冷運算,執行時間會被往後推遲。
const timer = setTimeout(() => {
  console.log('1.5 秒過去了...');
}, 1500);

// 如果反悔了,可以取消
// clearTimeout(timer);

setInterval:週期性重複任務

setInterval(callback, delay) 會每隔指定時間循環執行。

效能陷阱:如果回呼函式的執行時間(例如 2 秒)超過了間隔時間(例如 1 秒),會導致任務在隊列中堆積,消耗大量系統資源。 改善建議:對於非同步任務,推薦使用遞迴式的 setTimeout,確保上一次任務完成後才排程下一次。

setImmediate vs process.nextTick

這是 Node.js 開發者最容易混淆的兩個東西:

  • process.nextTick():不屬於事件迴圈的任何階段。它會在「目前階段結束、下一個階段開始前」插隊執行。優先權最高。
  • setImmediate():排在事件迴圈的 Check 階段。它通常在所有的 I/O 回呼之後執行。

進階控制:unref() 與 ref()

有時候你設定了一個長期的定時器(例如監控),但不希望它阻止整個 Node.js 進程退出。

  • timer.unref():告訴 Node.js,如果事件迴圈只剩下這個定時器還在跑,就直接退出進程吧。
  • timer.ref():恢復定時器的預設行為,即該定時器活著,進程就不會關閉。

現代開發首選:timers/promises

async/await 代碼中,使用回呼式的定時器會破壞代碼流暢度。Node.js 15+ 內建了 Promise 版本的工具。

const { setTimeout } = require('timers/promises');

async function demo() {
  console.log('準備開始...');

  // 就像同步代碼一樣暫停執行
  await setTimeout(2000);

  console.log('2 秒後的清爽時刻!');
}

面試常考題:執行順序大解析

setTimeout(() => console.log('A: setTimeout 0ms'), 0);
setImmediate(() => console.log('B: setImmediate'));
process.nextTick(() => console.log('C: nextTick'));
Promise.resolve().then(() => console.log('D: Promise'));

解析順序:

  1. C (nextTick):插隊之王,同步代碼跑完立刻執行。
  2. D (Promise):微任務 (Microtask),優先權僅次於 nextTick。
  3. A (setTimeout) / B (setImmediate):在 I/O 回呼中,B 必定先於 A;在主腳本中則由系統調度決定(通常 A 先)。

總結

  1. 盡情使用 timers/promises 來保持代碼簡潔。
  2. 若要進行週期性任務且涉及 I/O,請用遞迴 setTimeout 取代 setInterval。
  3. 善用 unref() 來優化長時間運行的後台任務監控。