本章介紹 ES6 兩個全新的特性:generator(生成器) 與 promise(承諾)。Generator 能產生一系列值並在每次產出後暫停執行;promise 則是非同步運算結果的佔位符。兩者結合後,可以用同步風格撰寫非同步程式碼,大幅提升可讀性與可維護性。
用 generators 與 promises 優雅化非同步程式碼#
書中以一個從遠端伺服器取得忍者任務資訊的情境為例,說明三種寫法的差異:
- 同步寫法:簡潔易讀、可用
try-catch處理錯誤,但會阻塞 UI - 回呼寫法:不阻塞,但巢狀層層疊加(pyramid of doom),錯誤處理繁瑣且醜陋
- generator + promise 寫法:在 generator 函式中使用
yield搭配回傳 promise 的getJSON,外觀近似同步程式碼,同時保有非阻塞特性
async(function* () {
try {
const ninjas = yield getJSON("ninjas.json");
const missions = yield getJSON(ninjas[0].missionsUrl);
const missionDescription = yield getJSON(missions[0].detailsUrl);
} catch (e) {
// 錯誤處理
}
});使用 generator 函式#
Generator 是一種全新的函式類型,與一般函式截然不同。它能按需(per request)逐一產生值序列,並在每次產值後暫停執行,等待下一次請求時從中斷處繼續。
- 定義方式:在
function關鍵字後加上星號* - 透過
yield關鍵字產出個別值

Figure 6.1: 在 function 關鍵字後加星號 (*) 來定義 generator
function* WeaponGenerator() {
yield "Katana";
yield "Wakizashi";
yield "Kusarigama";
}透過 iterator 物件控制 generator#
呼叫 generator 函式不會執行函式本體,而是建立一個 iterator(迭代器) 物件。透過 iterator 的 next() 方法請求值:
- 每次呼叫
next()會執行 generator 直到遇到yield,回傳{ value, done }物件 done為false表示還有更多值;為true表示 generator 已完成- 可用
while迴圈手動迭代,也可用for-of迴圈(語法糖)自動迭代
const weaponsIterator = WeaponGenerator();
const result1 = weaponsIterator.next(); // { value: "Katana", done: false }
const result2 = weaponsIterator.next(); // { value: "Wakizashi", done: false }
const result3 = weaponsIterator.next(); // { value: undefined, done: true }

Figure 6.2: 迭代 WeaponGenerator() 的結果
委派給另一個 generator#
使用 yield* 運算子可將執行委派給另一個 generator,所有對當前 iterator 的 next() 呼叫都會轉發到被委派的 generator,直到它完成為止。
function* WarriorGenerator() {
yield "Sun Tzu";
yield* NinjaGenerator(); // 委派
yield "Genghis Khan";
}實用場景#
- 產生唯一 ID:用無限
while(true)迴圈配合yield安全地逐一產出 ID,變數封閉在 generator 內部,不會被外部意外修改 - 遍歷 DOM 樹:用
yield*遞迴委派子節點的遍歷,將產出值與消費值的程式碼分離,可用簡單的for-of迴圈消費
在 generator 中使用無限迴圈是安全的——每遇到
yield就會暫停,直到外部呼叫next()才繼續,不會造成阻塞。
與 generator 雙向溝通#
Generator 不僅能從中取出值,還能傳入值,實現雙向通訊:
- 函式引數:建立 iterator 時傳入引數,如
NinjaGenerator("skulk") - 透過
next()傳值:呼叫next(value)時,傳入的值會成為 generator 中當前暫停的yield表達式的值
function* NinjaGenerator(action) {
const imposter = yield "Hattori " + action;
// imposter 的值來自第二次 next() 傳入的引數
}
const ninjaIterator = NinjaGenerator("skulk");
const result1 = ninjaIterator.next(); // { value: "Hattori skulk", done: false }
const result2 = ninjaIterator.next("Hanzo"); // imposter === "Hanzo"
第一次呼叫
next()時不能傳入有意義的值,因為此時還沒有等待中的yield表達式來接收。如需提供初始值,可透過 generator 函式的引數。
拋出例外到 generator#
每個 iterator 除了 next() 外,還有 throw() 方法,可從外部向 generator 拋入例外。搭配 generator 內部的 try-catch 區塊,可以優雅地處理錯誤。
const ninjaIterator = NinjaGenerator();
ninjaIterator.next();
ninjaIterator.throw("Catch this!"); // 在 generator 的 yield 處拋出例外

Figure 6.4: 可以從外部向 generator 拋出例外
深入 generator 底層機制#
Generator 像一個在多個狀態間轉換的小型狀態機:
- Suspended start:generator 建立後的初始狀態,尚未執行任何程式碼
- Executing:正在執行 generator 程式碼
- Suspended yield:遇到
yield暫停,等待下一次next()呼叫 - Completed:遇到
return或程式碼執行完畢

Figure 6.5: generator 執行期間在不同狀態間轉換
執行情境(execution context) 的關鍵差異:
- 一般函式回傳後,其 execution context 從堆疊彈出並丟棄
- Generator 的 execution context 從堆疊彈出後不會丟棄——iterator 物件保有對它的參考(類似 closure 的原理)
- 呼叫
next()時,該 execution context 被重新推入堆疊頂端,從中斷處繼續執行

Figure 6.6: 呼叫 NinjaGenerator 前後的執行上下文堆疊快照

Figure 6.8: 呼叫 iterator 的 next 方法會重新啟動 generator 的執行上下文

Figure 6.9: yield 值後,generator 的執行上下文從堆疊彈出但不被丟棄
使用 promises#
在 JavaScript 中,我們大量依賴非同步運算。Promise 是 ES6 引入的新型內建物件,作為一個「我們現在還沒有、但未來會有」的值的佔位符。
回呼的三大問題#
- 錯誤處理困難:callback 不在原始
try-catch的事件迴圈步驟中執行,錯誤容易遺失 - 序列步驟難以組合:相互依賴的非同步操作導致巢狀回呼(pyramid of doom),難以理解和維護
- 平行步驟難以協調:需要手動追蹤多個獨立非同步任務的完成狀態,大量樣板程式碼
深入 promise#
Promise 在生命週期中經歷以下狀態:
- Pending(待定):初始狀態,又稱 unresolved
- Fulfilled(已實現):呼叫
resolve()後,成功取得承諾的值 - Rejected(已拒絕):呼叫
reject()或發生未捕獲的例外
一旦 promise 到達 fulfilled 或 rejected 狀態,就不可逆轉。無法從 fulfilled 變為 rejected,反之亦然。統稱為 resolved 狀態。

Figure 6.10: Promise 的狀態轉換圖
建立 promise 的方式:
const ninjaPromise = new Promise((resolve, reject) => {
resolve("Hattori");
});
ninjaPromise.then(
(ninja) => {
/* 成功回呼 */
},
(err) => {
/* 失敗回呼 */
}
);Promise 的
then回呼總是非同步執行,即使 promise 已經被 resolve,回呼也會在當前事件迴圈步驟中的所有程式碼執行完畢後才被呼叫。

Figure 6.11: 執行 promise 範例的結果
拒絕 promise#
有兩種方式拒絕 promise:
- 顯式拒絕:在 executor 函式中呼叫
reject() - 隱式拒絕:在 promise 處理過程中發生未捕獲的例外
// 顯式
const promise = new Promise((resolve, reject) => {
reject("Explicitly reject a promise!");
});
// 隱式
const promise = new Promise((resolve, reject) => {
undeclaredVariable++; // 未捕獲的例外導致隱式拒絕
});可使用 .catch() 方法鏈接錯誤處理,效果等同於在 then() 中提供第二個回呼。
建立真實世界的 promise#
書中以 getJSON 函式為例,展示如何將 XMLHttpRequest 包裝成 promise:
- 建立
XMLHttpRequest物件並回傳新的 promise onload事件中:檢查狀態碼是否為 200,嘗試JSON.parse,成功則resolve,否則rejectonerror事件中:直接reject
鏈接 promise#
then() 方法會回傳一個新的 promise,因此可以無限鏈接:
getJSON("data/ninjas.json")
.then((ninjas) => getJSON(ninjas[0].missionsUrl))
.then((missions) => getJSON(missions[0].detailsUrl))
.then((mission) => {
/* 處理結果 */
})
.catch((error) => {
/* 統一錯誤處理 */
});鏈接中任何步驟的錯誤都能被末尾的 .catch() 統一捕獲。
等待多個 promise#
Promise.all:接收 promise 陣列,回傳新 promise。當所有傳入的 promise 都 resolve 時才 resolve,任一 reject 則整體 reject。成功回呼收到的是按傳入順序排列的結果陣列。Promise.race:接收 promise 陣列,回傳新 promise。以第一個 resolve 或 reject 的 promise 的結果作為整體結果。
Promise.all([
getJSON("data/ninjas.json"),
getJSON("data/mapInfo.json"),
getJSON("data/plan.json"),
])
.then((results) => {
/* results[0], results[1], results[2] */
})
.catch((error) => {
/* 任一失敗 */
});結合 generators 與 promises#
核心思路:將非同步任務放在 generator 中,每遇到非同步操作就 yield 一個 promise。外部的 async 輔助函式負責:
- 建立 iterator 並呼叫
next()啟動 generator - 收到 yield 出的 promise 後,註冊
then與catch回呼 - Promise resolve 時,用
iterator.next(value)將結果送回 generator - Promise reject 時,用
iterator.throw(error)拋出例外到 generator - 重複直到 generator 完成
這個模式結合了多項 JavaScript 特性:
- First-class functions:將 generator 函式作為引數傳遞
- Generator functions:利用暫停/恢復能力
- Promises:處理非同步操作
- Callbacks:在 promise 上註冊成功/失敗回呼
- Arrow functions:簡化回呼語法
- Closures:iterator 在
async函式中建立,在 promise 回呼中存取
展望:async 函式#
JavaScript 標準正在引入 async / await 關鍵字,將上述 generator + promise 的樣板程式碼內建到語言中:
(async function () {
try {
const ninjas = await getJSON("ninjas.json");
const missions = await getJSON(ninjas[0].missionsUrl);
console.log(missions);
} catch (e) {
console.log("Error: ", e);
}
})();async標記函式依賴非同步值await告訴引擎「請等待此結果,但不要阻塞」- 背景運作機制與前述的 generator + promise 組合完全相同
本章重點整理#
- Generator 按需逐一產生值序列,不像一般函式一次回傳單一值
- Generator 可以暫停與恢復執行,不會阻塞主執行緒
- 宣告方式為
function*,使用yield產出值,yield*委派給另一個 generator - 呼叫 generator 會建立 iterator 物件,透過
next()取值、throw()拋入例外、next(value)傳入值 - Promise 是非同步運算結果的佔位符,狀態為 pending / fulfilled / rejected,一旦 resolved 不可逆轉
- Promise 透過
then鏈接序列步驟,Promise.all處理平行步驟,Promise.race取第一個完成的結果 - 結合 generator 與 promise,可以用同步風格撰寫非同步程式碼
async/await是這個模式的語言層級內建支援