一句摘要:同步 Iterator 的 next() 立即回傳 Iterator Result;Async Iterator 則用 Promise 表示「下一筆還沒準備好」,而 for await...of 負責反覆等待並消費這些結果。
前置知識:理解 Iterator 的 next()、Promise 與 await。
學習路線:把同步 Iterator 延伸成 Async Iterator,並在同一篇接上對應的消費語法 for await...of。
標籤:ES2018 Async Iterator Async Iterable for await...of Promise

Day 11 建立了一個關鍵模型:
await會等待一個未來結果,結果就緒後,再恢復目前 async function 的後續工作。
但 Iterator 不只有一個結果。
consumer 每呼叫一次:
iterator.next();
都可能再取得一筆資料。
同步 Iterator 假設下一筆可以立即產生;如果下一筆來自網路、stream 或檔案,producer 卻可能還需要時間準備。
這時問題不再只是:
怎麼等待一個未來結果?
而是:
怎麼反覆等待未來的下一筆資料?
這就是 Async Iterator 與 for await...of 要一起解決的問題。
next() 立即回應取得結果先從熟悉的 Array iterator 開始:
const iterator = [10, 20, 30].values();
console.log(iterator.next());
// { value: 10, done: false }
同步 Iterator 的 next() 會立即回傳 Iterator Result:
{
value,
done
}
其中:
value 是這次交付的值;done 表示資料來源是否已經結束。完整過程可以畫成:
consumer 呼叫 next()
↓
producer 立即產生下一筆
↓
回傳 { value, done }
Generator 也遵守同一套協議:
function* numbers() {
yield 1;
yield 2;
yield 3;
}
const iterator = numbers();
iterator.next();
// { value: 1, done: false }
consumer 呼叫 next(),Generator 從上次暫停的位置繼續執行,直到遇到下一個 yield。
這裡隱含了一個重要前提:
producer 能在
next()回傳前,立刻決定下一個value與done。
現在把資料來源換成:
stream 的下一個 chunk
檔案的下一段內容
資料庫 cursor 的下一筆
持續連線的下一個 event
consumer 仍然會要求:
「請給我下一筆。」
但資料來源可能因為網路傳輸或等待後端伺服器回應
next()
↓
等待資料抵達
↓
產生下一筆
此時同步的行為介面就不夠了:
next() -> { value, done }
因為 next() 被呼叫的當下,可能還不知道 value,甚至還不知道資料來源是否已經結束。
但我們上次已經看過,JavaScript 已經有一個工具可以表示「現在還沒有,未來才會有的結果」:
Promise~
因此 Async Iterator 的核心變化是:
同步 Iterator
next()
↓
{ value, done }
變成:
Async Iterator
next()
↓
Promise
↓
{ value, done }
換成型別概念就是:
next() -> Promise<IteratorResult>
Iterator Result 本身沒有改,仍然是:
{
value,
done
}
改變的是取得它的時間。
consumer 必須這樣等待:
const result = await iterator.next();
所以 Async Iterator 不是另一套完全不同的資料模型,而是把原本的 Iterator protocol 延伸到非同步資料來源。
只有一個帶有 next() 的物件,可以稱為 Async Iterator。
如果希望物件能交給 for await...of,還要讓它成為 Async Iterable,也就是提供:
Symbol.asyncIterator
下面是一個可以直接執行的最小範例:
const wait = (ms) =>
new Promise((resolve) => setTimeout(resolve, ms));
function createDelayedValues(values, delay) {
return {
// 讓外層物件符合 Async Iterable protocol
[Symbol.asyncIterator]() {
let index = 0; // 每個 iterator 各自維護讀取進度
return {
// async function 一定回傳 Promise<IteratorResult>
async next() {
// 資料已讀完
if (index >= values.length) {
return { value: undefined, done: true };
}
// consumer 索取下一筆後,才開始等待並準備 模擬一下等待未來資料
await wait(delay);
return {
value: values[index++],
done: false,
};
},
};
},
};
}
const source = createDelayedValues(["A", "B", "C"], 500);
createDelayedValues() 回傳的物件是 Async Iterable:
source
↓ Symbol.asyncIterator()
Async Iterator
↓ next()
Promise<IteratorResult>
每次呼叫 source[Symbol.asyncIterator](),都會建立一個新的 iterator,並擁有自己的 index 狀態。
iterator 的
next()是async function,前一篇 Day 11 有提到因為內部可能await形成 async context,也是一種未來需要等待結果的狀態,所以設定為回傳Promise:
const iterator = source[Symbol.asyncIterator]();
const pendingResult = iterator.next();
console.log(pendingResult instanceof Promise);
// true
console.log(await pendingResult);
// 約 500ms 後:{ value: "A", done: false }
再呼叫一次,才會等待並取得 B:
console.log(await iterator.next());
// 約 500ms 後:{ value: "B", done: false }
當資料用完後:
console.log(await iterator.next());
// { value: undefined, done: true }
可以看到這裡仍然是熟悉的 pull-based model。
不是 producer 自己把三筆資料塞進 consumer,而是 consumer 每呼叫一次 next(),producer 才準備下一個 Iterator Result。
如果不用特殊語法,consumer 可以自己寫出完整迴圈:
const iterator = source[Symbol.asyncIterator]();
while (true) {
const result = await iterator.next();
if (result.done) {
break;
}
console.log(result.value);
}
執行順序是:
await iterator.next()
↓
等待 A
↓
console.log("A")
↓
await iterator.next()
↓
等待 B
↓
console.log("B")
↓
await iterator.next()
↓
等待 C
↓
console.log("C")
↓
await iterator.next()
↓
done: true,離開迴圈
這段程式清楚呈現 Async Iterator protocol,但每次使用都自己處理下面幾件事並不方便:
next();done;value;因此 JavaScript 提供了對應的消費語法。
for await...of 幫我們做了什麼?剛才的手動迴圈可以改寫成:
for await (const value of source) {
console.log(value);
}
大致可以先把它理解成:
const iterator = source[Symbol.asyncIterator]();
while (true) {
const result = await iterator.next();
if (result.done) {
break;
}
const value = result.value;
console.log(value);
}
for await...of 實際在協調什麼?規範使用 GetIterator、ForIn/OfBodyEvaluation、Await 等抽象操作;它不是可以直接貼進程式執行的 JavaScript。下面省略環境建立、destructuring 與 Completion Record 等細節,只保留理解 consumer 行為最重要的步驟。
| TC39 規範概念 | 白話步驟 | 對應到上面的近似程式 |
|---|---|---|
GetIterator(source, async) |
向 source 取得能非同步迭代的 iterator;通常會用到 Symbol.asyncIterator。 |
source[Symbol.asyncIterator]() |
呼叫 iterator 的 next 方法 |
consumer 主動要求「請給我下一筆」。 | iterator.next() |
Await(nextResult) |
下一筆尚未就緒時,暫停這次 loop 的後續工作,等 Promise settled。 | await iterator.next() |
檢查 Iterator Result 的 done |
若來源已結束,就不再執行 loop body。 | if (result.done) break |
取得 Iterator Result 的 value |
將這一筆值綁定給 loop 變數,執行你的程式。 | const value = result.value 與 console.log(value) |
AsyncIteratorClose |
若 break、return 或錯誤提早離開,嘗試關閉 iterator,讓它有機會清理資源。 |
後面的 return() 範例 |
所以可以把規範的主線白話化成:取得 async iterator → 要下一筆 → 等它回答 → 結束就離開,否則交給 loop body;若提早離開則嘗試收尾。
想查完整演算法時,可看 TC39 的 ForIn/OfHeadEvaluation、ForIn/OfBodyEvaluation 與 AsyncIteratorClose。
看到這裡會發現,跟同步的 iterator 關係非常對稱,差別在於回傳介面變成 Promise。
同步迭代:
Iterable
↓ Symbol.iterator()
Iterator
↓ next()
Iterator Result
↓
for...of
非同步迭代:
Async Iterable
↓ Symbol.asyncIterator()
Async Iterator
↓ next()
Promise<IteratorResult>
↓ await
for await...of
for await...of 並沒有讓 producer 變快,也沒有預先把所有資料收集起來。
它只是替 consumer 表達:
我會依序取得資料來源定義的下一筆;如果下一筆還沒準備好,我就在這裡等待。
Iterator protocol 不只處理正常走到 done: true 的情況。
如果 consumer 使用 break、return 或丟出錯誤而提早離開,for await...of 會嘗試呼叫 iterator 的 return(),同樣地讓外部流程有機會中止流程。
可以替剛才的 iterator 加上 return():
function createDelayedValues(values, delay) {
return {
[Symbol.asyncIterator]() {
let index = 0;
return {
async next() {
if (index >= values.length) {
return { value: undefined, done: true };
}
await wait(delay);
return {
value: values[index++],
done: false,
};
},
async return() {
console.log("停止後續工作");
return { value: undefined, done: true };
},
};
},
};
}
現在提早離開:
for await (const value of createDelayedValues(["A", "B", "C"], 500)) {
console.log(value);
if (value === "B") {
break;
}
}
輸出會是:
A
B
停止後續工作
這個清理能力對檔案、stream 或持續連線特別重要。
不過 return() 能清理到什麼程度,大部分仍然取決於後端API事件的實作。它可能只是釋放本地資源,也可能進一步取消底層工作;不能只看到 break 就假設遠端操作一定已經停止。
next() 失敗呢?Async Iterator 的 next() 回傳 Promise,所以等待下一筆時也可能 rejected。
例如:
const brokenSource = {
[Symbol.asyncIterator]() {
let count = 0;
return {
async next() {
count += 1;
if (count === 2) {
throw new Error("下一筆資料讀取失敗");
}
return {
value: "first",
done: false,
};
},
};
},
};
consumer 可以用一般的 try...catch 接住:
try {
for await (const value of brokenSource) {
console.log(value);
}
} catch (error) {
console.error(error.message);
}
輸出大致是:
first
下一筆資料讀取失敗
所以不需要替 for await...of 學一套全新的錯誤模型。
它仍然是:
next() 回傳的 Promise rejected
↓
await 拋出錯誤
↓
try...catch 接住
for await...of 也能讀同步 Iterablefor await...of 的主要用途是 Async Iterable,但它也能接收一般同步 Iterable,其實跟 Promise resolve 很像:
for await (const value of [1, 2, 3]) {
console.log(value);
}
JavaScript 會替同步 Iterator 建立非同步轉接。
這個能力有時能讓 consumer 同時接受同步與非同步來源;不過若資料本來就是普通 Array,直接使用 for...of 通常更清楚,也避免不必要的 Promise 等待。
同步 Iterator 的協議是:
next() -> { value, done }
Async Iterator 的協議是:
next() -> Promise<{ value, done }>
兩者都保留相同的控制方向:
consumer
↓ 要求下一筆
producer
↓ 準備資料
Iterator Result
差別只在結果能不能立即取得。
當下一筆還沒準備好,Promise 負責表示未來的 Iterator Result;for await...of 則負責反覆等待、檢查 done,並將 value 交給 loop body。
總而言之簡單心法可以這樣記:
for...of
= 反覆取得同步的下一筆
for await...of
= 反覆等待非同步的下一筆
慢慢細看這幾天的非同步內容,你會發現 Async Iterator 和同步 Iterator 的核心概念其實很接近:差別主要在於,取得下一筆資料時,Async Iterator 會用 Promise 包裝尚未準備好的結果。
但當資料持續流動,來源與處理端的速度也可能不同時,事情就不只是「等待下一筆」而已。接下來,就一起看看 Web 是如何處理這些持續流動資料的實際場景吧~一起加油!
ECMAScript Specification — Control Abstraction Objects
Async Iterator、Async Iterable 與 Async Generator 的正式規格。
MDN — AsyncIterator
Async Iterator protocol 與 next() 回傳值。
MDN — Symbol.asyncIterator
Async Iterable 如何提供預設 Async Iterator。
MDN — for await...of
Async Iterable 的消費語法、同步 Iterable fallback 與提早結束行為。
TC39 — Async Iteration proposal
Async iteration 的提案背景與設計。