iT邦幫忙

2026 iThome 鐵人賽

DAY 12
0
JavaScript

30 天新世代 JavaScript 自我學習指南系列 第 12

Day 12|從 async Iterator 協議到理解 for await...of

  • 分享至 

  • xImage
  •  

摘要

  • 一句摘要:同步 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


今日學習目標

  1. 理解 Async Iterator 的 next() 為何回傳 Promise。
  2. 了解如何手動等待下一筆資料。
  3. 學會用 for await...of 反覆消費非同步資料來源。

https://ithelp.ithome.com.tw/upload/images/20260914/20145251lUZuQ1Y6pP.png

Day 11 建立了一個關鍵模型:

await 會等待一個未來結果,結果就緒後,再恢復目前 async function 的後續工作。

但 Iterator 不只有一個結果。

consumer 每呼叫一次:

iterator.next();

都可能再取得一筆資料。

同步 Iterator 假設下一筆可以立即產生;如果下一筆來自網路、stream 或檔案,producer 卻可能還需要時間準備。

這時問題不再只是:

怎麼等待一個未來結果?

而是:

怎麼反覆等待未來的下一筆資料?

這就是 Async Iterator 與 for await...of 要一起解決的問題。


一、同步 Iterator 協議: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() 回傳前,立刻決定下一個 valuedone


二、如果下一筆還沒準備好呢?

現在把資料來源換成:

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 延伸到非同步資料來源。


三、完整的 Async Iterable 長什麼樣子?

只有一個帶有 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。


四、手動消費 Async Iterator

如果不用特殊語法,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,但每次使用都自己處理下面幾件事並不方便:

  • 取得 Async Iterator;
  • 反覆呼叫 next()
  • 等待 Promise;
  • 檢查 done
  • 讀取 value
  • 在提早離開時清理 iterator。

因此 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);
}

對照 TC39 規範:for await...of 實際在協調什麼?

規範使用 GetIteratorForIn/OfBodyEvaluationAwait 等抽象操作;它不是可以直接貼進程式執行的 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.valueconsole.log(value)
AsyncIteratorClose breakreturn 或錯誤提早離開,嘗試關閉 iterator,讓它有機會清理資源。 後面的 return() 範例

所以可以把規範的主線白話化成:取得 async iterator → 要下一筆 → 等它回答 → 結束就離開,否則交給 loop body;若提早離開則嘗試收尾。

想查完整演算法時,可看 TC39 的 ForIn/OfHeadEvaluationForIn/OfBodyEvaluationAsyncIteratorClose


看到這裡會發現,跟同步的 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 也有機會清理

Iterator protocol 不只處理正常走到 done: true 的情況。

如果 consumer 使用 breakreturn 或丟出錯誤而提早離開,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 也能讀同步 Iterable

for 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 是如何處理這些持續流動資料的實際場景吧~一起加油!


參考資料


上一篇
Day 11|await 暫停的到底是什麼?async function 如何交還控制權
下一篇
Day 13|從 Iterator 走向 Web Streams 的世界: 認識 Readable、Writable、Transform
系列文
30 天新世代 JavaScript 自我學習指南14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言