มีความแตกต่างระหว่างรอ Promise.all () และรออีกหลายครั้งหรือไม่


181

มีความแตกต่างระหว่าง:

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];

คำตอบ:


210

หมายเหตุ :

คำตอบนี้เพียงครอบคลุมความแตกต่างระหว่างระยะเวลาในซีรีส์และawait Promise.allโปรดอ่าน@ คำตอบที่ครอบคลุม mikep ที่ยังครอบคลุมถึงความแตกต่างที่สำคัญในการจัดการข้อผิดพลาด


สำหรับจุดประสงค์ของคำตอบนี้ฉันจะใช้วิธีการตัวอย่าง:

  • 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

ซึ่งหมายความว่าPromise.allจะแก้ไขด้วยข้อมูลจากคำสัญญาภายในหลังจาก 3 วินาที

แต่Promise.allมีพฤติกรรม "ล้มเหลวเร็ว" :

ตัวอย่างที่ 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-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


4
ดังนั้นโดยทั่วไปความแตกต่างเป็นเพียงคุณสมบัติ "ไม่เร็ว" ของ Promise.all?
แมทธิว

4
@mclzc ในตัวอย่าง # 3 การเรียกใช้โค้ดอีกต่อไปถูกหยุดจนกว่าการแก้ไขล่าช้า 1 มันยังอยู่ในข้อความ "ถ้าคุณใช้ async-คอยแทนคุณจะต้องรอให้สัญญาแต่ละครั้งแก้ไขตามลำดับ"
haggis

1
@Qback มีข้อมูลโค้ดสดที่แสดงพฤติกรรม ลองเรียกใช้และอ่านรหัสอีกครั้ง คุณไม่ใช่คนแรกที่เข้าใจผิดว่าลำดับของสัญญามีลักษณะอย่างไร ความผิดพลาดที่คุณทำในการสาธิตคือคุณไม่ได้เริ่มสัญญาในเวลาเดียวกัน
zzzzBov

1
@zzzzBov คุณพูดถูก คุณกำลังเริ่มต้นในเวลาเดียวกัน ขออภัยฉันมาที่คำถามนี้ด้วยเหตุผลอื่นและฉันมองข้ามไป
Qback

2
" อาจไม่มีประสิทธิภาพ " - และที่สำคัญกว่านั้นทำให้เกิดunhandledrejectionข้อผิดพลาด คุณจะไม่ต้องการใช้สิ่งนี้ โปรดเพิ่มสิ่งนี้ในคำตอบของคุณ
Bergi

88

ความแตกต่างแรก - ล้มเหลวอย่างรวดเร็ว

ฉันเห็นด้วยกับคำตอบของ @ zzzzBov แต่ข้อดี "ไม่เร็ว" ของ Promise.all ไม่ได้เป็นเพียงข้อแตกต่าง ผู้ใช้บางคนในความคิดเห็นถามว่าเหตุใดจึงต้องใช้ Promise.all เมื่อมันเร็วขึ้นในสถานการณ์เชิงลบเท่านั้น (เมื่องานบางงานล้มเหลว) และฉันถามว่าทำไมไม่ หากฉันมีสองงานแบบอะซิงโครนัสแบบอิสระแยกกันและงานชิ้นแรกได้รับการแก้ไขในเวลานานมาก แต่งานที่สองถูกปฏิเสธในเวลาสั้น ๆ เหตุใดผู้ใช้จึงต้องรอให้เกิดข้อผิดพลาด "นานมาก" แทนที่จะเป็น ในการใช้งานจริงเราต้องพิจารณาสถานการณ์เชิงลบ แต่ตกลง - ในความแตกต่างแรกนี้คุณสามารถเลือกได้ว่าจะใช้ทางเลือกใดกับ Promise.all

ข้อแตกต่างที่สอง - การจัดการข้อผิดพลาด

แต่เมื่อพิจารณาข้อผิดพลาดในการจัดการคุณต้องใช้ Promise.all เป็นไปไม่ได้ที่จะจัดการข้อผิดพลาดของงานแบบขนาน async ที่ถูกกระตุ้นด้วยการรอหลายครั้งอย่างถูกต้อง ในสถานการณ์เชิงลบคุณจะจบลงด้วยUnhandledPromiseRejectionWarningและPromiseRejectionHandledWarningแม้ว่าคุณจะใช้ลอง / จับได้ทุกที่ นั่นคือเหตุผลที่ Promise.all ได้รับการออกแบบ แน่นอนว่าบางคนสามารถพูดได้ว่าเราสามารถระงับข้อผิดพลาดที่ใช้process.on('unhandledRejection', err => {})และprocess.on('rejectionHandled', err => {})มันไม่ใช่วิธีปฏิบัติที่ดี ฉันพบตัวอย่างมากมายบนอินเทอร์เน็ตที่ไม่พิจารณาข้อผิดพลาดในการจัดการ async แบบขนานอย่างอิสระสองงานหรือมากกว่าหรือพิจารณา แต่ในทางที่ผิด - เพียงแค่ใช้ลอง / จับและหวังว่ามันจะจับข้อผิดพลาด แทบเป็นไปไม่ได้เลยที่จะหาวิธีปฏิบัติที่ดี นั่นคือเหตุผลที่ฉันเขียนคำตอบนี้

สรุป

อย่าใช้งานหลาย ๆ อย่างรอคอยสำหรับงาน async แบบขนานอย่างน้อยสองงานเนื่องจากคุณจะไม่สามารถจัดการข้อผิดพลาดได้อย่างจริงจัง ใช้ Promise.all () เสมอสำหรับกรณีการใช้งานนี้ Async / await ไม่ได้มาแทนที่ Promises มันเป็นวิธีการใช้สัญญาสวย ๆ ... รหัส async ถูกเขียนในรูปแบบการซิงค์และเราสามารถหลีกเลี่ยงได้หลายอย่างthenในสัญญา

บางคนบอกว่าการใช้ Promise.all () เราไม่สามารถจัดการข้อผิดพลาดของงานแยกต่างหาก แต่เกิดจากข้อผิดพลาดจากสัญญาที่ถูกปฏิเสธครั้งแรก (ใช่บางกรณีการใช้งานอาจต้องการการจัดการแยกต่างหากเช่นสำหรับการบันทึก) ไม่มีปัญหา - ดูหัวข้อ "การเพิ่ม" ด้านล่าง

ตัวอย่าง

พิจารณาภารกิจ async นี้ ...

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 และหลายรอ ตัวอย่างทั้งสองลงท้ายด้วยTask 1 succeed! Task 2 succeed!หลังจาก 5 วินาที

// 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!

ในขณะที่คุณสามารถเห็นข้อผิดพลาดในการจัดการที่ประสบความสำเร็จเราจำเป็นต้องเพิ่มหนึ่ง catch to runfunction และโค้ดที่มีลอจิก catch อยู่ใน callback ( async style ) เราไม่จำเป็นต้องจัดการกับข้อผิดพลาดภายในrunฟังก์ชั่นเพราะฟังก์ชั่น async มันทำโดยอัตโนมัติ - การปฏิเสธสัญญาของtaskฟังก์ชั่นทำให้เกิดการปฏิเสธrunฟังก์ชั่น เพื่อหลีกเลี่ยงการโทรกลับเราสามารถใช้รูปแบบการซิงค์ (async / await + try / catch) try { await run(); } catch(err) { }แต่ในตัวอย่างนี้มันเป็นไปไม่ได้เพราะเราไม่สามารถใช้awaitในเธรดหลักได้ - มันสามารถใช้ได้เฉพาะในฟังก์ชั่น async (เป็นตรรกะเพราะไม่มีใครต้องการ บล็อกเธรดหลัก) เพื่อทดสอบว่าการจัดการทำงานในลักษณะซิงค์เราสามารถโทรrunฟังก์ชั่นจากฟังก์ชั่น async อื่นหรือใช้ IIFE (รื้อฟื้นทันที Expression (async function() { try { await run(); } catch(err) { console.log('Caught error', err); }; })();ฟังก์ชั่น):

นี่เป็นวิธีที่ถูกต้องเพียงวิธีเดียวในการเรียกใช้งาน async แบบขนานสองงานขึ้นไปและจัดการข้อผิดพลาด คุณควรหลีกเลี่ยงตัวอย่างด้านล่าง


// 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เป็น async

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)

... เหมือนข้างบน ผู้ใช้ @Qwerty ในคำตอบที่ถูกลบของเขาถามเกี่ยวกับพฤติกรรมแปลก ๆ ที่ดูเหมือนจะถูกจับ แต่ก็มีข้อผิดพลาดที่ไม่สามารถจัดการได้ เราตรวจจับข้อผิดพลาดเนื่องจาก run () ถูกปฏิเสธตามคำหลักที่รอและสามารถจับได้โดยใช้ try / catch เมื่อเรียกใช้ run () นอกจากนี้เรายังได้รับข้อผิดพลาดที่ไม่สามารถจัดการได้เพราะเรากำลังเรียกฟังก์ชั่นงาน async แบบซิงโครนัส มันก็คล้าย ๆ เมื่อเราไม่สามารถที่จะจัดการกับข้อผิดพลาดโดยลอง / จับเมื่อเรียกฟังก์ชั่นการซิงค์บางส่วนซึ่งเป็นส่วนหนึ่งของรหัสที่วิ่งใน setTimeout function test() { setTimeout(function() { console.log(causesError); }, 0); }; try { test(); } catch(e) { /* this will never catch error */ }...

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)

... ข้อผิดพลาดสองข้อ "เท่านั้น" (อันที่สามหายไป) แต่ไม่พบสิ่งใด


เพิ่มเติม (จัดการข้อผิดพลาดในงานแยกต่างหากและข้อผิดพลาดที่เกิดจากความล้มเหลวก่อน)

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ใช้เพื่อยิงข้อผิดพลาดสุดท้าย)


14
คำตอบนี้ดีกว่าคำตอบที่ยอมรับเพราะคำตอบที่ยอมรับในขณะนี้พลาดหัวข้อที่สำคัญมากของการจัดการข้อผิดพลาด
chrishiestand

8

โดยทั่วไปการใช้Promise.all()คำขอ "async" แบบขนาน การใช้awaitสามารถเรียกใช้ในแบบขนานหรือเป็นการ "ซิงค์" การปิดกั้น

ฟังก์ชั่นtest1และtest2ด้านล่างแสดงวิธีawaitเรียกใช้ async หรือซิงค์

test3แสดงให้เห็นPromise.all()ว่าเป็น async

jsfiddle พร้อมผลการจับเวลา - เปิดคอนโซลของเบราว์เซอร์เพื่อดูผลการทดสอบ

พฤติกรรมการซิงค์ ไม่ทำงานแบบขนานใช้เวลา ~ 1800ms :

const test1 = async () => {
  const delay1 = await Promise.delay(600); //runs 1st
  const delay2 = await Promise.delay(600); //waits 600 for delay1 to run
  const delay3 = await Promise.delay(600); //waits 600 more for delay2 to run
};

พฤติกรรมของAsync ทำงานแบบParalelใช้เวลา ~ 600ms :

const test2 = async () => {
  const delay1 = Promise.delay(600);
  const delay2 = Promise.delay(600);
  const delay3 = Promise.delay(600);
  const data1 = await delay1;
  const data2 = await delay2;
  const data3 = await delay3; //runs all delays simultaneously
}

พฤติกรรมของAsync ทำงานแบบขนานใช้เวลา ~ 600ms :

const test3 = async () => {
  await Promise.all([
  Promise.delay(600), 
  Promise.delay(600), 
  Promise.delay(600)]); //runs all delays simultaneously
};

TLDR; หากคุณกำลังใช้งานPromise.all()มันจะ "หยุดทำงานเร็ว" - หยุดทำงานในเวลาที่เกิดความล้มเหลวครั้งแรกของฟังก์ชั่นใด ๆ ที่มีให้


1
ฉันจะขอคำอธิบายโดยละเอียดเกี่ยวกับสิ่งที่เกิดขึ้นภายใต้ประทุนในตัวอย่าง 1 และ 2 ได้อย่างไร ฉันประหลาดใจมากที่สิ่งเหล่านี้มีวิธีการทำงานที่แตกต่างไปจากที่ฉันคาดหวังว่าพฤติกรรมจะเหมือนกัน
Gregordy

2
@Gregordy ใช่มันน่าแปลกใจ ฉันโพสต์คำตอบนี้เพื่อบันทึก coders ใหม่เพื่อ async ปวดหัวบางอย่าง มันคือทั้งหมดที่เกี่ยวกับเมื่อ JS ประเมินการรอคอยนี่คือเหตุผลที่คุณกำหนดตัวแปรอย่างไร การอ่าน Async เชิงลึก: blog.bitsrc.io/…
GavinBelson

7

คุณสามารถตรวจสอบด้วยตัวคุณเอง

ในซอนี้ฉันรันการทดสอบเพื่อแสดงให้เห็นถึงลักษณะการปิดกั้นของawaitซึ่งตรงข้ามกับPromise.allที่จะเริ่มสัญญาทั้งหมดและในขณะที่หนึ่งรอมันจะไปกับคนอื่น ๆ


6
อันที่จริงซอของคุณไม่ได้ตอบคำถามของเขา มีความแตกต่างระหว่างการเรียกเป็นt1 = task1(); t2 = task2()และจากนั้นใช้awaitหลังจากนั้นทั้งสองคนresult1 = await t1; result2 = await t2;เหมือนในคำถามของเขาเมื่อเทียบกับสิ่งที่คุณกำลังทดสอบซึ่งมีการใช้awaitresult1 = await task1(); result2 = await task2();ในการเรียกร้องเดิมเช่น รหัสในคำถามของเขาจะเริ่มสัญญาทั้งหมดในครั้งเดียว ความแตกต่างเช่นคำตอบแสดงให้เห็นคือความล้มเหลวจะได้รับรายงานเร็วขึ้นด้วยPromise.allวิธีการ
BryanGrezeszak

คำตอบของคุณอยู่นอกหัวข้อเช่น @BryanGrezeszak แสดงความคิดเห็น คุณควรลบมันเพื่อหลีกเลี่ยงการทำให้ผู้ใช้เข้าใจผิด
mikep

0

ในกรณีที่รอ Promise.all ([task1 (), task2 ()]); "task1 ()" และ "task2 ()" จะทำงานแบบขนานและจะรอจนกว่าคำสัญญาทั้งสองจะเสร็จสิ้น (แก้ไขหรือปฏิเสธ) ในกรณีที่

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

t2 จะทำงานหลังจาก t1 ดำเนินการเสร็จสิ้นแล้ว (ได้รับการแก้ไขหรือปฏิเสธ) ทั้ง t1 และ t2 จะไม่ทำงานแบบขนาน

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.