Skip to content

2026-05-13

/ 14 分鐘閱讀

/ JavaScript Hard Parts v3

Promises、Async 與 Event Loop:為什麼 JavaScript 需要非同步機制

這個章節將會包含以下四個核心主題:

  • Promises:ES6 最重要的特性之一
  • Asynchronicity(非同步性):讓動態網頁應用成為可能的關鍵機制
  • Event Loop:JavaScript 的優先排程系統(triage)
  • Microtask queue、Callback queue 與 Web Browser APIs

JavaScript 的執行模型回顧

在進入非同步之前,必須先確認 JavaScript 的基本執行特性。以下面的程式碼為例:

javascript
const num = 3;

function multiplyBy2(inputNumber) {
    const result = inputNumber * 2;
    return result;
}

const output = multiplyBy2(num);
const newOutput = multiplyBy2(10);

JavaScript 執行這段程式碼時,必須完整進入 multiplyBy2 的執行環境、完成計算並取得回傳值後,才會繼續執行下一行。它不會在等待 multiplyBy2 的同時跳去執行其他事情。

這反映出 JavaScript 兩個核心特性:

  • Single-threaded(單執行緒):同一時間只有一個指令在執行
  • Synchronously executed(同步執行):每一行程式碼依照出現順序依序執行

問題:同步模型遇上耗時任務

現代網頁應用往往需要與外部伺服器溝通,例如向 TikTok 的伺服器請求影片連結。這類網路請求可能耗費數百毫秒。若 JavaScript 以同步方式等待回應,後續所有程式碼都必須暫停執行:

javascript
const videos = getVideos('http://tiktok.com/will/1');
// 🚫 等待 350ms,請求送達 TikTok 伺服器

displayVideos(videos);
// 必須等 videos 取得後才能執行

// 其他程式碼
console.log('I want to runnnn!');

挑戰在於:我們需要等待 videos 資料回來才能呼叫 displayVideos,但在等待期間,其他所有程式碼(包括 console.log)都無法執行。

setTimeout 能解決問題嗎?

直覺上可能會想到使用 setTimeout 來延遲執行:

javascript
function printHello() {
    console.log('Hello');
}

// 延遲 1000ms 後執行 printHello
setTimeout(printHello, 1000);

console.log('Me first!');

若按照 JavaScript 的同步執行模型推理,應該要先等 1000ms 讓計時完成,再執行 printHello,最後才跑 console.log("Me first!"),輸出順序應為:

Hello
Me first!

但實際結果卻是:

Me first!
Hello

更令人困惑的是,即使把延遲設為 0 毫秒,結果依然相同:

javascript
setTimeout(printHello, 0);
console.log('Me first!');
// 輸出:Me first! → Hello

這說明光靠「執行緒、記憶體、Call Stack」三者組成的傳統 JavaScript 模型,已經無法解釋實際的執行行為。

JavaScript 並非獨立運行

JavaScript 實際上執行於瀏覽器或 Node.js 環境之中,這些宿主環境提供了許多 JavaScript 本身並不具備的功能。JavaScript 引擎的核心三元件為:

元件說明
Thread of Execution逐行執行程式碼的執行緒
Memory(Variable Environment)儲存應用程式當下所有資料的空間
Call Stack追蹤目前正在執行哪個函式

僅靠這三者不足以支撐現代網頁應用,必須加入以下新元件:

  • Web Browser APIs / Node APIs(後台功能)
  • Promises
  • Event Loop
  • Callback Queue(Task Queue)
  • Microtask Queue

Web Browser 提供的功能(不屬於 JavaScript 本身)

瀏覽器環境提供了大量 JavaScript 本身並不內建的能力,包括:

功能JavaScript 中的存取標籤
DOMdocument
計時器(Timers)setTimeout / setInterval
網路請求fetch(舊版為 XMLHttpRequest / XHR
Consoleconsole
Local StoragelocalStorage
IndexedDBindexedDB
Service WorkersService Worker API
攝影機 / 麥克風Web APIs

這些看起來像是 JavaScript 函式,實際上是幻象函式(Facade Functions),它們的外表像是在做 JavaScript 的工作,但本質上是觸發了 JavaScript 執行環境之外的瀏覽器層級功能。

例如,setTimeout 在 JavaScript 這一側幾乎什麼都不做,它真正啟動的是瀏覽器內建的計時器功能,計時完成後再透過特定機制把回呼函式送回 JavaScript 執行。這正是為何 0ms 延遲的 setTimeout 仍然會在同步程式碼後才執行,其背後的排程機制即是 Event LoopCallback Queue,這些將在後續章節中詳細展開。

複習

為什麼以下程式碼即使延遲設定為 0 毫秒,仍然不會在「me first」之前印出「hello」?

javascript
function printHello() {
    console.log('hello');
}
setTimeout(printHello, 0);
console.log('me first');

儘管延遲設定為 0 毫秒,setTimeout 本質上是一項 Web Browser 功能,運作於 JavaScript 正規執行模型之外。這段程式碼說明了 JavaScript 的單執行緒模型(執行緒、記憶體、Call Stack)並不足以解釋非同步行為。實際的執行順序是由 Callback Queue 與 Event Loop 等額外元件所決定的。

小測驗

為什麼在 JavaScript 中等待網路請求會造成問題? 網路請求會阻塞所有其他程式碼的執行
下列哪一項並非 JavaScript 本身的功能,而是瀏覽器提供的功能? DOM

此文章是 FrontendMasters 上的 The Hard Parts of JavaScript 課程筆記

最後更新時間:

Buy Me A Coffee

系列章節 第 27 篇 / 共 27 篇

0 %
MIT Licensed | Copyright © 2025-present Wen-Hsiu's Blog
Photo by Federica Galli on Unsplash