網站搜尋

javascript - 我如何在不使用『快速失敗』行為的情況下同時等待多個 Promise?


我正在使用 async/await 來並行發送多個 api 調用:

async function foo(arr) {
  const results = await Promise.all(arr.map(v => {
     return doAsyncThing(v)
  }))
  return results
}

我知道,與 loops 不同,Promise.all 是並行執行的(也就是說,等待結果的部分是並行的)。

但我也知道:

如果其中一個元素被拒絕,Promise.all 就會被拒絕,並且 Promise.all 會快速失敗:如果你有四個在超時後解決的 Promise,而其中一個立即被拒絕,則 Promise.all 會立即被拒絕。

當我閱讀這個時,如果我使用 Promise.all 與 5 個 promises,而最先完成的一個返回 reject(),那麼其他 4 個實際上被取消,它們承諾的 resolve() 值就會丟失。

有沒有第三種方式?執行實際上是並行的,但一次失敗不會毀掉整個批次?

答案#1

雖然被接受的答案中的方法可以解決你的問題,但這是一種反模式。用錯誤來解決一個承諾並不是好的做法,而且有更乾淨的方法可以做到這一點。

你想做的事情,用偽代碼表示,就是:

fn task() {
  result-1 = doAsync();
  result-n = doAsync();

  // handle results together
  return handleResults(result-1, ..., result-n)
}

這可以簡單地透過 async/await 達成,而不需要使用 Promise.all。一個可行的範例:

console.clear();

function wait(ms, data) {
  return new Promise( resolve => setTimeout(resolve.bind(this, data), ms) );
}

/** 
 * These will be run in series, because we call
 * a function and immediately wait for each result, 
 * so this will finish in 1s.
 */
async function series() {
  return {
    result1: await wait(500, 'seriesTask1'),
    result2: await wait(500, 'seriesTask2'),
  }
}

/** 
 * While here we call the functions first,
 * then wait for the result later, so 
 * this will finish in 500ms.
 */
async function parallel() {
  const task1 = wait(500, 'parallelTask1');
  const task2 = wait(500, 'parallelTask2');

  return {
    result1: await task1,
    result2: await task2,
  }
}

async function taskRunner(fn, label) {
  const startTime = performance.now();
  console.log(`Task ${label} starting...`);
  let result = await fn();
  console.log(`Task ${label} finished in ${ Number.parseInt(performance.now() - startTime) } milliseconds with,`, result);
}

void taskRunner(series, 'series');
void taskRunner(parallel, 'parallel');

注意:您需要一個啟用了 async/await 的瀏覽器才能運行此程式碼片段。

這樣你就可以簡單地使用 try/catch 來處理你的錯誤,並在 parallel 函數內返回部分結果。

答案#2

ES2020 包含 Promise.allSettled,它能做到你想要的。

Promise.allSettled([
    Promise.resolve('a'),
    Promise.reject('b')
]).then(console.log)

輸出:

[
  {
    "status": "fulfilled",
    "value": "a"
  },
  {
    "status": "rejected",
    "reason": "b"
  }
]

但如果你想「自己動手做」,那麼你可以利用這一點:使用 Promise#catch 意味著該 promise 會被解決(除非你在 catch 中拋出異常或手動拒絕 promise 鏈),因此你不需要明確返回一個已解決的 promise。

所以,只要使用 catch 處理錯誤,你就可以達到你想要的效果。

請注意,如果您希望錯誤在結果中可見,您將必須決定一種呈現它們的慣例。

你可以使用 Array#map 對集合中的每個 promise 應用拒絕處理函數,並使用 Promise.all 等待它們全部完成。

範例

以下內容應該會被列印出來:

Elapsed Time   Output

     0         started...
     1s        foo completed
     1s        bar completed
     2s        bam errored
     2s        done [
                   "foo result",
                   "bar result",
                   {
                       "error": "bam"
                   }
               ]

async function foo() {
    await new Promise((r)=>setTimeout(r,1000))
    console.log('foo completed')
    return 'foo result'
}

async function bar() {
    await new Promise((r)=>setTimeout(r,1000))
    console.log('bar completed')
    return 'bar result'
}

async function bam() {
    try {
        await new Promise((_,reject)=>setTimeout(reject,2000))
    } catch {
        console.log('bam errored')
        throw 'bam'
    }
}

function handleRejection(p) {
    return p.catch((error)=>({
        error
    }))
}

function waitForAll(...ps) {
    console.log('started...')
    return Promise.all(ps.map(handleRejection))
}

waitForAll(foo(), bar(), bam()).then(results=>console.log('done', results))

參見。