iT邦幫忙

2026 iThome 鐵人賽

DAY 17
1
JavaScript

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

Day 17|已經有 for await...of,為什麼還需要 Array.fromAsync?

  • 分享至 

  • xImage
  •  

摘要

ES2024 的 Array.fromAsync() 把「用 for await...of 讀完整個來源並逐筆 push()」整理成標準 API,適合將最終會結束的 Async Iterable 收集成 Promise<Array>

前置知識:有空可以先讀讀 Day 12|從 async Iterator 協議到理解 for await...of ,認識基本概念就行。

學習路線:承接 Day 12–16 已建立的 producer/consumer 與 stream lifecycle,從「逐筆處理」延伸到「完整收集成 Array」。

標籤ES2024 Array.fromAsync Async Iterable Async Generator for await...of Promise


今日學習目標

  1. 說明 Array.fromAsync() 補上了哪一個標準庫缺口。
  2. 比較它與 for await...of 何時把控制權交回 consumer,而不是只比較語法長短。
  3. 理解 Array.fromAsync() 會逐筆取得、等待並收集資料。
  4. 判斷什麼時候應該收成 Array,什麼時候應該維持逐筆處理。

https://ithelp.ithome.com.tw/upload/images/20260920/201452512rjalEqv7O.png

前面幾天探討得比較多:資料來源方怎麼把「等待下一筆」交給 consumer 去 pull 獲得資料,甚至伺服器透過協議好的 SSE 事件主動 push 資料。補了一些基礎後,今天終於可以換個角度看幾個近期 ECMAScript 補上的 consumer 端工具。

而有時候 consumer 的需求非常單純:

我不需要在 loop 裡做特殊控制,只想把這個最終會結束的 Async Iterable 全部收成 Array。

const results = [];

for await (const value of asyncIterable) {
  results.push(value);
}

到了 ES2024,可以把這個意圖直接寫成:

const results = await Array.fromAsync(asyncIterable);

這就是 Array.fromAsync() 最核心的定位。

它沒有發明新的非同步資料流,也不是要取代 for await...of。它只是把一個常見動作整理成標準 API:

把可以逐筆等待的來源
完整收集成 Array

一、Array.fromAsync 可以想成在補齊 Array.from 功能

先回到同步世界。

如果有一個 Iterable:

const values = new Set([1, 2, 3]);

可以手動收集:

const result = [];

for (const value of values) {
  result.push(value);
}

也可以直接使用:

const result = Array.from(values);

console.log(result);
// [1, 2, 3]

Array.from() 很清楚地表達:

請把這個來源轉成 Array。


但思考一下為什麼 Async Iterable 不能交給 Array.from()

後面會一直用 async function* 造出這種來源,先花三行認識它:它是 async function 與 Generator 的合體,await 用來等下一筆資料到齊,yield 則把準備好的那筆交出去。呼叫它不會馬上執行,而是回傳一個 Async Iterable,等你用 for await...of 逐筆取。

async function* createValues() {
  yield 1;
  yield 2;
  yield 3;
}

Array.from(createValues());
// []

createValues() 交出的是 Async Iterable,Array.from() 卻收不到東西。

這裡有個容易誤會的地方:你可能會以為 Array.from() 有去拿資料,只是拿到的是還沒 resolve 的 Promise。但結果不是 [Promise, Promise, Promise],而是空的 []——它連 next() 都沒呼叫過。

原因藏在 Array.from() 的判斷順序:

  1. 它先找同步Symbol.iterator,找到才迭代;
  2. 找不到就退回把參數當成 array-like,只看 .length 和數字索引。
  3. 而 async generator 物件身上只有 Symbol.asyncIterator,沒有同步版:
const gen = createValues();

gen[Symbol.iterator];      // undefined ← Array.from 要找的,沒有
gen[Symbol.asyncIterator]; // function  ← 有的是這個,但 Array.from 不認
gen.length;                // undefined ← 退回 array-like,length 當 0

所以 Array.from() 看不到同步 iterator,就把 gen 當成一個「length 是 0 的 array-like」,直接產出空陣列。不是「拿到 Promise 沒等」,而是根本沒去迭代非同步來源。

TC39 對兩組 API 的關係有一個很精準的整理:

Array.from()      對應 for...of
Array.fromAsync() 對應 for await...of

因此:

const result = await Array.fromAsync(createValues());

console.log(result);
// [1, 2, 3]

Array.fromAsync() 可以先理解成 Array.from() 在非同步資料來源上的補完。


二、Array.fromAsync 回傳是 Promise<Array>

比較這兩行:

const syncResult = Array.from([1, 2, 3]);
const asyncResult = Array.fromAsync(createValues()); // Promise { <pending> }

第一行可以立刻取得 Array。

第二行呼叫時,來源可能還沒有產生任何資料,所以 Array.fromAsync() 會先回傳 Promise:

console.log(asyncResult instanceof Promise);
// true

這裡要特別分清楚:asyncResult一個 pending Promise,不是「3 個 Promise 的陣列」。

即使來源有 3 筆資料,逐筆等待都包在這一個 Promise<Array> 內部,由 Array.fromAsync() 自己處理完,最後一次交出完整陣列。所以你只需要 await 一次,拿到的就是純值 [1, 2, 3],不會是一堆還沒 resolve 的 Promise。

等到來源結束,Promise 才會 fulfilled,值才是完整 Array:

const values = await asyncResult;

console.log(values);
// [1, 2, 3]

它的回傳型態可以記成:

Async Iterable
      │
      │ Array.fromAsync()
      ▼
Promise<Array>

這也表示:如果來源永遠不結束,這個 Promise 就永遠不會得到完整 Array。


三、它大致等於 for await...of + push()

假設有一個逐筆產生報表區塊的 Async Generator:

async function* generateReport() {
  yield '摘要';
  yield '圖表';
  yield '結論';
}

for await...of 收集:

const sections = [];

for await (const section of generateReport()) {
  sections.push(section);
}

console.log(sections);
// ['摘要', '圖表', '結論']

使用 Array.fromAsync()

const sections = await Array.fromAsync(
  generateReport(),
);

console.log(sections);
// ['摘要', '圖表', '結論']

如果經常需要這個動作,可能會自己寫 helper:

async function toArray(asyncIterable) {
  const result = [];

  for await (const value of asyncIterable) {
    result.push(value);
  }

  return result;
}

Array.fromAsync() 的價值之一,就是不需要每個專案都重新實作這個 helper。

不過「大致等於」不代表所有規範細節都只是一個簡單 loop。標準 API 還會統一處理來源取得、Promise rejection、mapping callback、iterator 關閉與不同輸入類型。

舉個上面那個 toArray helper 會踩到的例子:如果來源不是 iterable,而是array-like object(只有 length 和數字索引、沒有 iterator),for await...of 直接就 throw:

const arrayLike = { length: 2, 0: Promise.resolve('X'), 1: Promise.resolve('Y') };

await toArray(arrayLike);
// TypeError: src is not async iterable

await Array.fromAsync(arrayLike);
// ['X', 'Y'] ← 不但收得到,還幫你把元素的 Promise 也 await 了

這種「不同輸入類型」的相容,正是標準 API 幫你統一掉、helper 得自己補的細節之一。

初學階段先建立這個心智模型就夠了:

取得下一筆
→ 等待這一筆
→ 放進 Array
→ 再取得下一筆
→ 直到 done: true

四、Array.fromAsync 可以在收集資料過程中逐筆轉換

Array.from() 一樣,Array.fromAsync() 可以接收 mapping callback:

const sections = await Array.fromAsync(
  generateReport(),
  async (section, index) => {
    return `${index + 1}. ${section}`;
  },
);

console.log(sections);
// ['1. 摘要', '2. 圖表', '3. 結論']

mapping callback 回傳 Promise 也可以;Array.fromAsync() 會等待 callback 的結果,再把它放進 Array。

這相當於:

const sections = [];
let index = 0;

for await (const section of generateReport()) {
  const mapped = await formatSection(section, index);
  sections.push(mapped);
  index += 1;
}

可以簡化成:

const sections = await Array.fromAsync(
  generateReport(),
  formatSection,
);

這個 API 表達的是一條很明確的流程:

逐筆取得
→ 逐筆轉換
→ 等待轉換結果
→ 收進最終 Array

五、實際案例:依序處理檔案,最後取得完整結果

假設使用者一次選了多個圖片檔案;每一個檔案都要先壓縮,再上傳:

const uploadedFiles = await Array.fromAsync(
  fileInput.files,
  async (file) => {
    const compressed = await compressImage(file);
    return uploadFile(compressed);
  },
);

這段程式會依序處理 mapping:

取得檔案 1
→ 等待壓縮與上傳
→ 放入結果 Array
→ 取得檔案 2
→ 等待壓縮與上傳
→ 放入結果 Array
→ ...

最後才得到完整的 uploadedFiles

這種寫法適合:

  • 檔案數量有限。
  • 每一筆都要等待非同步轉換。
  • 不希望同時啟動太多 CPU 或網路工作。
  • 下一個步驟需要完整結果 Array。

但要注意,「依序」不一定永遠比較好。如果每個工作彼此獨立,而且需求就是盡快同時完成,後面會看到 Promise.all() 可能更合適。


六、完整收集,還是把控制權留給 consumer?

如果需求只是完整收集:

const results = await Array.fromAsync(source);

它比手動 loop 更直接,因為程式碼清楚表達「我要 Array」。

但如果需要控制每一筆資料,for await...of 反而更自然。

最大差異:控制權什麼時候交回 consumer?

可以把最大差異記成:

for await...of 每一輪都把控制權交回 consumer;Array.fromAsync() 要等來源結束,才把最終結果交給 consumer。

如果說在每一筆非同步資料處理流程中,需要做一些額外邏輯處理:

每完成一筆就更新畫面

for await (const file of files) {
  const result = await uploadFile(file);
  showUploadedFile(result);
}

跳過不符合條件的資料

for await (const file of files) {
  if (!isSupportedImage(file)) {
    continue;
  }

  await uploadFile(file);
}

達成條件就提早停止

for await (const result of searchResults) {
  if (result.score >= 90) {
    console.log('找到足夠好的結果', result);
    break;
  }
}

這些控制流程不適合硬塞進 Array.fromAsync() 的 mapping callback。

所以不能只說 Array.fromAsync() 是「比較直觀的 for await...of」。更精準的說法是:

當需求只是完整收集時,Array.fromAsync() 對意圖的表達更直接;需要控制流程時,for await...of 更清楚。


七、不是所有 Async Iterable 都應該收成 Array

Day 15 看過 SSE 或 streaming SDK。這類來源可能持續很久,甚至資料沒有預定結束階段:

for await (const event of eventStream) {
  renderEvent(event);
}

如果改成:

const events = await Array.fromAsync(eventStream);

可能會出現兩個問題:

  1. 來源尚未結束,所以一直拿不到完整 Array。
  2. 所有 event 都會留在記憶體裡,資料越來越多。

大型檔案或大量資料也一樣,response body 即使最後會結束,把所有 chunk 留到最後仍可能浪費記憶體。

因此 Array.fromAsync() 適合的是 最終會結束的來源:不必事先知道資料數量,但它必須最終回傳 done: true,而且 consumer 確實需要完整結果。

一般 UI pagination 也通常不需要把所有頁面抓完:畫面只需要使用者目前要看的頁面時,就只抓那一頁;不要為了示範 Array.fromAsync() 而耗盡每一頁並增加不必要的 API 請求。


八、和 Promise.all() 的差異:一個逐筆 pull,一個整批等待

先回顧 Day 12 的 consumer 視角

  • for await...of 是消費者在每一筆到達時執行處理。
  • Array.fromAsync() 保留這種逐筆取得的順序,但把每筆值累積到最後才交回。
  • Promise.all() 適用於工作已經建立、彼此獨立,且需要一個整批結果的情況。

使用 Array.fromAsync() 搭配 async mapping:

const results = await Array.fromAsync(
  files,
  uploadFile,
);

mapping 結果會逐筆等待。

使用 Promise.all()

const jobs = [...files].map(uploadFile);
const results = await Promise.all(jobs);

map(uploadFile) 會先走完整個 Array,通常代表多個上傳工作已經被建立;Promise.all() 再等待全部結果。

更關鍵的差異:一個是續發,一個是並列

上面兩段程式碼長得很像,但執行方式完全不同,差別就藏在「mapping 什麼時候被呼叫」:

  • Array.fromAsync() 的 mapping 是逐筆呼叫的——前一筆 await 完,才呼叫下一筆的 uploadFile。所以多個上傳是續發(sequential):同一時間只有一個在跑。

  • Promise.all() 這一側,map(uploadFile)同步地一次走完整個 Array,等於瞬間把所有 uploadFile() 都呼叫、全部啟動;Promise.all() 只是接著等它們。所以多個上傳是並列(concurrent):同一時間全部在跑。

Array.fromAsync(files, uploadFile)
  上傳 1 ──✅──▶ 上傳 2 ──✅──▶ 上傳 3      (續發:一次一個)

Promise.all(files.map(uploadFile))
  上傳 1 ─────▶
  上傳 2 ─────▶  (並列:同時啟動)
  上傳 3 ─────▶

這個差別有實際後果:

  • 續發天然限制了同時進行的工作量——不會一次對伺服器打出大量請求,也適合「後一筆依賴前一筆結果」或「前一步失敗就不該做下一步」的流程。
  • 並列通常較快,但要自己控制並行度,否則可能一次送出過多請求、壓垮下游或耗盡資源。

換句話說:Array.fromAsync() 沒有 Promise.all() 那種「一次全開」的能力,因為它的 mapping 本來就是為了逐筆、確保順序而設計的。

使用上可能不是只問哪個比較快,需要針對不同流程去做考量:

  1. producer 是一次建立全部工作,還是 consumer 需要逐筆 pull?
  2. 最後需要完整 Array,還是每一筆抵達就要處理?

九、它還能接收哪些來源?

Array.fromAsync() 的輸入不只 Async Iterable:它也可以接收同步 Iterable(例如 Array、Set、Generator,包含會 yield Promise 的來源)與具有 length、數字索引的 array-like object。

例如同步 Generator 產生 Promise:

function* createTasks() {
  yield Promise.resolve('A');
  yield Promise.resolve('B');
  yield Promise.resolve('C');
}

const results = await Array.fromAsync(createTasks());

console.log(results);
// ['A', 'B', 'C']

這些 Promise 會依序被等待,再放進結果 Array。

完整理解不同輸入的規範細節並不是這篇的主線。最重要的使用情境仍然是:

最終會結束的可等待來源
→ 完整收成 Array

十、錯誤會讓回傳的 Promise rejected

如果來源在迭代途中失敗:

async function* brokenSource() {
  yield 'A';
  throw new Error('讀取失敗');
}

const resultPromise = Array.fromAsync(brokenSource());

resultPromise 會 rejected,不會取得一個只包含 'A' 的部分 Array:

try {
  const results = await resultPromise;
  console.log(results);
} catch (error) {
  console.error(error.message);
  // '讀取失敗'
}

mapping callback 失敗也是一樣:

const results = await Array.fromAsync(
  source,
  async (value) => {
    return transform(value);
  },
);

transform(value) throw 或回傳 rejected Promise,整個 Array.fromAsync() 的 Promise 就會 rejected。

因為是續發,失敗會「就地中止」

延續第八節的續發概念:Array.fromAsync() 是逐筆取得、逐筆等待的,所以某一筆失敗時,迭代就停在那個點——失敗點之後的工作根本不會啟動

取 A → await ✅ → push
取 B → await ❌ throw → 立刻中止,reject
         ↑
      後面的 C、D 永遠不會被取得

這正是續發的一個好處,也是它和 Promise.all() 的實際差別:Promise.all() 是並列的,失敗時已經啟動的請求還會在背景跑完(送出去收不回);Array.fromAsync() 則是還沒輪到的工作直接不做。這讓它天然適合「前一步失敗,後面就不該做」的交易式流程。

中止的同時,Array.fromAsync() 也會呼叫來源的 return() 把 iterator 正確關閉。所以如果來源的 generator 裡有 try/finallyfinally 仍然會執行,清理不會被漏掉:

async function* source() {
  try {
    yield 'A';
    throw new Error('boom');
  } finally {
    console.log('清理完成'); // 中止時仍會跑到
  }
}

所以它提供的是「完整成功後得到 Array」的模型,不是自動保留部分成功結果的工具。


今日結語

Array.fromAsync() 的價值不是取代 loop,而是補齊一個標準庫缺口——讓「把來源收成 Array」這件事,在非同步世界也有一個現成的答案:

Array.from()      把同步來源收成 Array
Array.fromAsync() 把非同步來源收成 Promise<Array>

而它和另外兩個常被拿來比較的 API,其實各自回答不同的問題。可以記成這張 consumer 地圖:

你的需求 該用
一組已經建立好、彼此獨立的工作,只要一個整批結果 Promise.all()
要在每一筆抵達時就處理,需要 skip/break/逐筆更新畫面 for await...of
一個會結束的 Async Iterable,要收成一份完整 Array Array.fromAsync()

三者的分野可以用兩個問題快速定位:工作是一次全開還是逐筆 pull?最後要的是完整 Array 還是每筆即時處理? 也別忘了——這三個 API 都不會自動替 producer 決定並行度:

  • Array.fromAsync()for await...of 是續發
  • Promise.all() 是並列,要不要限制同時進行的工作量,仍然是你自己的選擇。

到這邊來看整段非同步流程中資料的流動看了滿多事情,從逐筆 pull、到完整收集,我們把「怎麼消費一個非同步來源」走過了一輪,慢慢建立自己的心智藍圖吧~繼續加油。


參考資料


上一篇
Day 16|深入 Stream 本體:從 chunk、controller 到讀懂 Hono 的 StreamingApi
下一篇
Day 18|Promise.withResolvers:理解從「資料抵達」到「資料流到達可用節點」的轉變
系列文
30 天新世代 JavaScript 自我學習指南18
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

1
DanSnow
iT邦好手 1 級 ‧ 2026-09-21 22:13:17

挺有趣的,之前我沒特別看 Array.fromAsync 在做什麼 ,還以為它是像 Promise.all 那樣的平行執行,看來並不是

我要留言

立即登入留言