web-dev-qa-db-ja.com

Promise.all()を待つことと複数を待つことの違いは何ですか?

違いはありますか?

const [result1, result2] = await Promise.all([task1(), task2()]);

そして

const t1 = task1();
const t2 = task2();

const result1 = await t1;
const result2 = await t2;

そして

const [t1, t2] = [task1(), task2()];
const [result1, result2] = [await t1, await t2];
88
Hidden

この答えの目的のために、私はいくつかの方法例を使用します。

  • res(ms)はミリ秒の整数を取り、そのミリ秒後に解決する約束を返す関数です。
  • rej(ms)はミリ秒の整数を取り、そのミリ秒後に拒絶する約束を返す関数です。

resを呼び出すとタイマーが開始されます。少しの遅延を待つためにPromise.allを使用すると、すべての遅延が終了した後で解決されますが、同時に実行されることを忘れないでください。

例1
const data = await Promise.all([res(3000), res(2000), res(1000)])
//                              ^^^^^^^^^  ^^^^^^^^^  ^^^^^^^^^
//                               delay 1    delay 2    delay 3
//
// ms ------1---------2---------3
// =============================O delay 1
// ===================O           delay 2
// =========O                     delay 3
//
// =============================O Promise.all
async function example() {
  const start = Date.now()
  let i = 0
  function res(n) {
    const id = ++i
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve()
        console.log(`res #${id} called after ${n} milliseconds`, Date.now() - start)
      }, n)
    })
  }

  const data = await Promise.all([res(3000), res(2000), res(1000)])
  console.log(`Promise.all finished`, Date.now() - start)
}

example()

これはPromise.allが3秒後に内部の約束からのデータで解決されることを意味します。

しかし、 Promise.allは "fail fast"の振る舞いをします

例2
const data = await Promise.all([res(3000), res(2000), rej(1000)])
//                              ^^^^^^^^^  ^^^^^^^^^  ^^^^^^^^^
//                               delay 1    delay 2    delay 3
//
// ms ------1---------2---------3
// =============================O delay 1
// ===================O           delay 2
// =========X                     delay 3
//
// =========X                     Promise.all
async function example() {
  const start = Date.now()
  let i = 0
  function res(n) {
    const id = ++i
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve()
        console.log(`res #${id} called after ${n} milliseconds`, Date.now() - start)
      }, n)
    })
  }
  
  function rej(n) {
    const id = ++i
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        reject()
        console.log(`rej #${id} called after ${n} milliseconds`, Date.now() - start)
      }, n)
    })
  }
  
  try {
    const data = await Promise.all([res(3000), res(2000), rej(1000)])
  } catch (error) {
    console.log(`Promise.all finished`, Date.now() - start)
  }
}

example()

代わりにasync-awaitを使用する場合、それぞれの約束が順番に解決されるのを待たなければならないでしょう。

例3
const delay1 = res(3000)
const delay2 = res(2000)
const delay3 = rej(1000)

const data1 = await delay1
const data2 = await delay2
const data3 = await delay3

// ms ------1---------2---------3
// =============================O delay 1
// ===================O           delay 2
// =========X                     delay 3
//
// =============================X await
async function example() {
  const start = Date.now()
  let i = 0
  function res(n) {
    const id = ++i
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve()
        console.log(`res #${id} called after ${n} milliseconds`, Date.now() - start)
      }, n)
    })
  }
  
  function rej(n) {
    const id = ++i
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        reject()
        console.log(`rej #${id} called after ${n} milliseconds`, Date.now() - start)
      }, n)
    })
  }
  
  try {
    const delay1 = res(3000)
    const delay2 = res(2000)
    const delay3 = rej(1000)

    const data1 = await delay1
    const data2 = await delay2
    const data3 = await delay3
  } catch (error) {
    console.log(`await finished`, Date.now() - start)
  }
}

example()
123
zzzzBov

最初の違い - 早く失敗する

私は@ zzzzBovの答えに同意しますが、Promise.allの「失敗するのが早い」という利点が唯一の違いではありません。コメントで何人かのユーザーはそれが否定的なシナリオでより速いときだけPromise.allを使用する理由を尋ねます(いくつかのタスクが失敗するとき)。そして私はなぜそうではないのですか? 2つの独立した非同期並列タスクがあり、最初のタスクが非常に長い時間で解決されたが、2番目のエラータスクが非常に短い時間ではなく「非常に長い時間」待機する理由実際のアプリケーションでは、否定的なシナリオを考慮する必要があります。しかし、OK - この最初の違いでは、Promise.allと複数の方法のどちらを使用するかを決定できます。

第二の違い - エラー処理

しかし、エラー処理を検討するときは、Promise.allを使用しなければなりません。複数の待ち状態で発生した非同期並列タスクのエラーを正しく処理することはできません。否定的なシナリオでは、どこでもtry/catchを使用しますが、常にUnhandledPromiseRejectionWarningPromiseRejectionHandledWarningで終わります。それがPromise.allが設計された理由です。もちろん、process.on('unhandledRejection', err => {})process.on('rejectionHandled', err => {})を使ってそのエラーを抑制できると誰かが言うこともできますが、それは良い習慣ではありません。私はインターネット上で、2つ以上の独立した非同期並列タスクのエラー処理をまったく考慮しないか、または誤った方法で考慮しない多くの例を見つけました - ちょうどtry/catchを使用してエラーを捕捉することを期待します。良い習慣を見つけることはほとんど不可能です。私がこの答えを書いているのはそのためです。

概要

2つ以上の独立した非同期並列タスクには、多重要求を絶対に使用しないでください。エラーを真剣に処理できないからです。この使用例では、必ずPromise.all()を使用してください。Async/awaitは、Promiseの代わりにはなりません。これはpromiseの使い方のちょうどいい方法です...非同期コードはsyncスタイルで書かれていて、promiseに複数のthenを避けることができます。

Promise.all()を使用してタスクのエラーを個別に処理することはできず、最初に拒否されたプロミスからのエラーのみを処理することができると言う人もいます。問題ではありません - 下記の「追加」の見出しを参照してください。

この非同期タスクを検討してください...

const task = function(taskNum, seconds, negativeScenario) {
  return new Promise((resolve, reject) => {
    setTimeout(_ => {
      if (negativeScenario)
        reject(new Error('Task ' + taskNum + ' failed!'));
      else
        resolve('Task ' + taskNum + ' succeed!');
    }, seconds * 1000)
  });
};

前向きなシナリオでタスクを実行するとき、Promise.allと複数待機の間に違いはありません。両方の例とも5秒後にTask 1 succeed! Task 2 succeed!で終わります。

// Promise.all alternative
const run = async function() {
  // tasks run immediate in parallel and wait for both results
  let [r1, r2] = await Promise.all([
    task(1, 5, false),
    task(2, 5, false)
  ]);
  console.log(r1 + ' ' + r2);
};
run();
// at 5th sec: Task 1 succeed! Task 2 succeed!
// multiple await alternative
const run = async function() {
  // tasks run immediate in parallel
  let t1 = task(1, 5, false);
  let t2 = task(2, 5, false);
  // wait for both results
  let r1 = await t1;
  let r2 = await t2;
  console.log(r1 + ' ' + r2);
};
run();
// at 5th sec: Task 1 succeed! Task 2 succeed!

最初のタスクが肯定的なシナリオで10秒かかり、秒のタスクが否定的なシナリオで5秒かかる場合、発行されるエラーに違いがあります。

// Promise.all alternative
const run = async function() {
  let [r1, r2] = await Promise.all([
      task(1, 10, false),
      task(2, 5, true)
  ]);
  console.log(r1 + ' ' + r2);
};
run();
// at 5th sec: UnhandledPromiseRejectionWarning: Error: Task 2 failed!
// multiple await alternative
const run = async function() {
  let t1 = task(1, 10, false);
  let t2 = task(2, 5, true);
  let r1 = await t1;
  let r2 = await t2;
  console.log(r1 + ' ' + r2);
};
run();
// at 5th sec: UnhandledPromiseRejectionWarning: Error: Task 2 failed!
// at 10th sec: PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1)
// at 10th sec: UnhandledPromiseRejectionWarning: Error: Task 2 failed!

ここでは、複数の待機を並行して使用しているときに問題が発生していることに既に気付いているはずです。もちろんエラーを避けるために私たちはそれを扱うべきです!やってみよう...


// Promise.all alternative
const run = async function() {
  let [r1, r2] = await Promise.all([
    task(1, 10, false),
    task(2, 5, true)
  ]);
  console.log(r1 + ' ' + r2);
};
run().catch(err => { console.log('Caught error', err); });
// at 5th sec: Caught error Error: Task 2 failed!

エラーをうまく処理できるように見えるので、run関数にただ1つのcatchを追加する必要があり、catchロジックを持つコードはコールバック内にあります(非同期スタイル)。 run関数内でハンドルエラーを処理する必要はありません。非同期関数が自動的に実行するからです。task関数の棄却はrun関数の却下を引き起こします。コールバックを避けるために、同期スタイル(async/await + try/catch)try { await run(); } catch(err) { }を使用できますが、この例ではメインスレッドでawaitを使用できないため不可能です。非同期関数でのみ使用されます(誰もメインスレッドをブロックしたくないので論理的です)。処理が同期スタイルで機能するかどうかをテストするには、別の非同期関数からrun関数を呼び出すか、IIFE(即時呼び出し関数式):(async function() { try { await run(); } catch(err) { console.log('Caught error', err); }; })();を使用します。

これは、2つ以上の非同期並列タスクを実行してエラーを処理する方法の1つにすぎません。下記の例は避けてください。


// multiple await alternative
const run = async function() {
  let t1 = task(1, 10, false);
  let t2 = task(2, 5, true);
  let r1 = await t1;
  let r2 = await t2;
  console.log(r1 + ' ' + r2);
};

いくつかの方法で上記のコードを処理しようとすることができます...

try { run(); } catch(err) { console.log('Caught error', err); };
// at 5th sec: UnhandledPromiseRejectionWarning: Error: Task 2 failed!
// at 10th sec: UnhandledPromiseRejectionWarning: Error: Task 2 failed!
// at 10th sec: PromiseRejectionHandledWarning: Promise rejection was handled 

...同期コードを処理しますがrunは非同期です

run().catch(err => { console.log('Caught error', err); });
// at 5th sec: UnhandledPromiseRejectionWarning: Error: Task 2 failed!
// at 10th sec: Caught error Error: Task 2 failed!
// at 10th sec: PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1)

……?最初に、タスク2のエラーは処理されず、後でそれが捕捉されたことがわかります。誤解を招くような、まだコンソールのエラーがいっぱいです。このように使えません。

(async function() { try { await run(); } catch(err) { console.log('Caught error', err); }; })();
// at 5th sec: UnhandledPromiseRejectionWarning: Error: Task 2 failed!
// at 10th sec: Caught error Error: Task 2 failed!
// at 10th sec: PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1)

...上と同じです。

const run = async function() {
  try {
    let t1 = task(1, 10, false);
    let t2 = task(2, 5, true);
    let r1 = await t1;
    let r2 = await t2;
  }
  catch (err) {
    return new Error(err);
  }
  console.log(r1 + ' ' + r2);
};
run().catch(err => { console.log('Caught error', err); });
// at 5th sec: UnhandledPromiseRejectionWarning: Error: Task 2 failed!
// at 10th sec: PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1)

...「2つだけ」2つのエラー(3つ目のエラーはありません)が検出されました。


追加(タスクエラーを個別に処理し、また最初に失敗したエラーも処理する)

const run = async function() {
  let [r1, r2] = await Promise.all([
    task(1, 10, true).catch(err => { console.log('Task 1 failed!'); throw err; }),
    task(2, 5, true).catch(err => { console.log('Task 2 failed!'); throw err; })
  ]);
  console.log(r1 + ' ' + r2);
};
run().catch(err => { console.log('Run failed (does not matter which task)!'); });
// at 5th sec: Task 2 failed!
// at 5th sec: Run failed (does not matter which task)!
// at 10th sec: Task 1 failed!

...この例では、何が起こるかをよりよく示すために両方のタスクにnegativeScenario = trueを使用しました(throw errは最終的なエラーを発生させるために使用されます)。

19
mikep

あなたは自分自身をチェックすることができます。

この フィドル では、すべての約束を開始し、待機中のPromise.allとは対照的に、awaitのブロッキング性を実証するためのテストを実行しました。それは他の人たちと共に進みます。

4
zpr