iT邦幫忙

2026 iThome 鐵人賽

DAY 17
1
JavaScript

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

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

  • 分享至 

  • xImage
  •  

摘要

ES2026 的 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」。

標籤:ES2026 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/finally,finally 仍然會執行,清理不會被漏掉:

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 自我學習指南 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

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

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

我要留言

立即登入留言