Có sự khác biệt nào giữa await Promise.all () và nhiều await không?


Câu trả lời:


209

Lưu ý :

Câu trả lời này chỉ bao gồm sự khác biệt về thời gian giữa awaitloạt và Promise.all. Hãy chắc chắn đọc câu trả lời toàn diện của @ mikep cũng bao gồm những khác biệt quan trọng hơn trong xử lý lỗi .


Với mục đích của câu trả lời này, tôi sẽ sử dụng một số phương pháp ví dụ:

  • res(ms) là một hàm lấy một số nguyên mili giây và trả về một lời hứa sẽ giải quyết sau nhiều mili giây đó.
  • rej(ms) là một hàm lấy một số nguyên mili giây và trả về một lời hứa từ chối sau nhiều mili giây đó.

Cuộc gọi resbắt đầu hẹn giờ. Sử dụng Promise.allđể chờ một số độ trễ sẽ giải quyết sau khi tất cả các độ trễ kết thúc, nhưng hãy nhớ rằng chúng thực thi cùng một lúc:

Ví dụ 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

Điều này có nghĩa rằng Promise.all sẽ giải quyết với dữ liệu từ những lời hứa bên trong sau 3 giây.

Nhưng, Promise.allcó hành vi "thất bại nhanh" :

Ví dụ # 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

Nếu bạn sử dụng async-awaitthay thế, bạn sẽ phải chờ từng lời hứa giải quyết tuần tự, điều này có thể không hiệu quả:

Ví dụ # 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
Vậy về cơ bản, sự khác biệt chỉ là tính năng "thất bại nhanh" của Promise.all?
Matthew

4
@mclzc Trong ví dụ # 3 việc thực thi mã tiếp tục bị dừng cho đến khi delay1 giải quyết. Ngay cả trong văn bản "Nếu bạn sử dụng async-await thay vào đó, bạn sẽ phải chờ từng lời hứa để giải quyết tuần tự"
haggis

1
@Qback, có một đoạn mã trực tiếp thể hiện hành vi. Xem xét việc chạy nó và đọc lại mã. Bạn không phải là người đầu tiên hiểu sai về cách hành xử của những lời hứa. Sai lầm bạn đã mắc phải trong bản demo là bạn không bắt đầu lời hứa cùng một lúc.
zzzzBov

1
@zzzzBov Bạn nói đúng. Bạn đang bắt đầu nó cùng một lúc. Xin lỗi, tôi đến câu hỏi này vì một lý do khác và tôi đã bỏ qua điều đó.
Qback

2
" nó có thể không hiệu quả " - và quan trọng hơn là gây ra unhandledrejectionlỗi. Bạn sẽ không bao giờ muốn sử dụng này. Vui lòng thêm điều này vào câu trả lời của bạn.
Bergi

87

Khác biệt đầu tiên - thất bại nhanh

Tôi đồng ý với câu trả lời của @ zzzzBov nhưng lợi thế "không nhanh" của Promise.all không chỉ là một điểm khác biệt. Một số người dùng trong các bình luận hỏi tại sao nên sử dụng Promise.all khi nó chỉ nhanh hơn trong kịch bản tiêu cực (khi một số tác vụ thất bại). Và tôi hỏi tại sao không? Nếu tôi có hai tác vụ song song không đồng bộ độc lập và lần đầu tiên được giải quyết trong thời gian rất dài nhưng lần thứ hai bị từ chối trong thời gian rất ngắn, tại sao lại để người dùng chờ đợi thông báo lỗi "thời gian rất dài" thay vì "thời gian rất ngắn"? Trong các ứng dụng thực tế, chúng ta phải xem xét kịch bản tiêu cực. Nhưng OK - trong sự khác biệt đầu tiên này, bạn có thể quyết định lựa chọn nào để sử dụng Promise.all so với nhiều chờ đợi.

Khác biệt thứ hai - xử lý lỗi

Nhưng khi xem xét xử lý lỗi BẠN PHẢI sử dụng Promise.all. Không thể xử lý chính xác các lỗi của các tác vụ song song không đồng bộ được kích hoạt với nhiều chờ đợi. Trong trường hợp tiêu cực, bạn sẽ luôn kết thúc UnhandledPromiseRejectionWarningPromiseRejectionHandledWarningmặc dù bạn sử dụng thử / bắt ở bất cứ đâu. Đó là lý do tại sao Promise.all được thiết kế. Tất nhiên ai đó có thể nói rằng chúng ta có thể ngăn chặn lỗi đó bằng cách sử dụng process.on('unhandledRejection', err => {})process.on('rejectionHandled', err => {}) đó không phải là thực tiễn tốt. Tôi đã tìm thấy nhiều ví dụ trên internet không xem xét xử lý lỗi đối với hai hoặc nhiều tác vụ song song không đồng bộ độc lập hoặc xem xét sai nhưng chỉ sử dụng thử / bắt và hy vọng nó sẽ bắt lỗi. Hầu như không thể tìm thấy thực hành tốt. Đó là lý do tại sao tôi viết câu trả lời này.

Tóm lược

Không bao giờ sử dụng nhiều chờ đợi cho hai hoặc nhiều tác vụ song song không đồng bộ độc lập vì bạn sẽ không thể xử lý lỗi nghiêm trọng. Luôn sử dụng Promise.all () cho trường hợp sử dụng này. Async / await không thay thế cho Promise. Đây chỉ là một cách khá hay để sử dụng lời hứa ... mã async được viết theo kiểu đồng bộ hóa và chúng ta có thể tránh được nhiềuthen lời hứa.

Một số người nói rằng sử dụng Promise.all () chúng tôi không thể xử lý các lỗi nhiệm vụ một cách riêng biệt mà chỉ có lỗi từ lời hứa bị từ chối đầu tiên (vâng, một số trường hợp sử dụng có thể yêu cầu xử lý riêng, ví dụ như để đăng nhập). Đây không phải là vấn đề - xem tiêu đề "Bổ sung" bên dưới.

Ví dụ

Hãy xem xét nhiệm vụ không đồng bộ này ...

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)
  });
};

Khi bạn chạy các tác vụ trong kịch bản tích cực, không có sự khác biệt giữa Promise.all và nhiều sự chờ đợi. Cả hai ví dụ kết thúc Task 1 succeed! Task 2 succeed!sau 5 giây.

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

Khi tác vụ đầu tiên mất 10 giây trong kịch bản tích cực và nhiệm vụ giây mất 5 giây trong kịch bản tiêu cực, có sự khác biệt về lỗi được đưa ra.

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

Chúng ta nên lưu ý ở đây rằng chúng ta đang làm gì đó sai khi sử dụng song song nhiều chờ đợi. Tất nhiên để tránh lỗi chúng ta nên xử lý nó! Hãy thử ...


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

Như bạn có thể thấy để xử lý thành công lỗi, chúng ta cần thêm chỉ một runhàm vào hàm và mã với logic bắt là trong hàm gọi lại ( kiểu async ). Chúng tôi không cần xử lý các lỗi bên trong runchức năng vì chức năng async tự động thực hiện - việc từ chối taskchức năng gây ra từ chối runchức năng. Để tránh gọi lại, chúng tôi có thể sử dụng kiểu đồng bộ hóa (async / await + try / Catch) try { await run(); } catch(err) { }nhưng trong ví dụ này không thể vì chúng tôi không thể sử dụng awaittrong luồng chính - nó chỉ có thể được sử dụng trong chức năng async (nó hợp lý vì không ai muốn khối chủ đề chính). Để kiểm tra xem việc xử lý có hoạt động trong chức năng kiểu đồng bộ hóa chúng ta có thể gọirun từ một chức năng không đồng bộ khác hay sử dụng IIFE (Biểu thức chức năng được gọi ngay lập tức) : (async function() { try { await run(); } catch(err) { console.log('Caught error', err); }; })();.

Đây chỉ là một cách chính xác để chạy hai hoặc nhiều tác vụ song song không đồng bộ và xử lý lỗi. Bạn nên tránh các ví dụ dưới đây.


// 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);
};

Chúng tôi có thể cố gắng xử lý mã ở trên một số cách ...

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 

... không có gì bị bắt vì nó xử lý đang sync nhưng runlà 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)

... Wtf? Trước tiên, chúng tôi thấy rằng lỗi cho nhiệm vụ 2 đã không được xử lý và sau đó đã bị bắt. Gây hiểu lầm và vẫn đầy lỗi trong giao diện điều khiển. Không thể sử dụng theo cách này.

(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)

... như trên. Người dùng @Qwerty trong câu trả lời đã xóa của mình đã hỏi về hành vi kỳ lạ này dường như bị bắt nhưng cũng có những lỗi chưa được xử lý. Chúng tôi bắt lỗi vì run () bị từ chối trên dòng với từ khóa đang chờ và có thể bị bắt bằng cách sử dụng try / Catch khi gọi run (). Chúng tôi cũng nhận được lỗi chưa được xử lý vì chúng tôi đang gọi hàm tác vụ async một cách đồng bộ (không có từ khóa chờ) và tác vụ này chạy bên ngoài hàm run () và cũng bị lỗi bên ngoài. Nó tương tự như khi chúng tôi không thể xử lý lỗi bằng try / catch khi gọi một số chức năng đồng bộ hóa mà một phần của mã chạy trong 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)

... "Chỉ" hai lỗi (thiếu thứ 3) nhưng không có gì bị bắt.


Ngoài ra (xử lý các lỗi nhiệm vụ riêng biệt và cả lỗi đầu tiên)

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!

... lưu ý rằng trong ví dụ này tôi đã sử dụng negScenario = true cho cả hai tác vụ để thể hiện tốt hơn những gì xảy ra ( throw errđược sử dụng để bắn lỗi cuối cùng)


14
câu trả lời này tốt hơn câu trả lời được chấp nhận vì câu trả lời hiện được chấp nhận bỏ lỡ chủ đề rất quan trọng về xử lý lỗi
chrishiestand

8

Nói chung, sử dụng Promise.all()chạy các yêu cầu "không đồng bộ" song song. Sử dụng awaitcó thể chạy song song HOẶC bị chặn "đồng bộ hóa".

Các hàm test1test2 bên dưới hiển thị cách awaitcó thể chạy async hoặc đồng bộ hóa.

test3 cho thấy Promise.all()đó là async.

jsfiddle với kết quả được hẹn giờ - mở bảng điều khiển trình duyệt để xem kết quả kiểm tra

Đồng bộ hóa hành vi. KHÔNG chạy song song, mất ~ 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 hành vi. Chạy trong paralel, mất ~ 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 hành vi. Chạy song song, mất ~ 600ms :

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

TLDR; Nếu bạn đang sử dụng, Promise.all()nó cũng sẽ "nhanh hỏng" - ngừng chạy tại thời điểm lỗi đầu tiên của bất kỳ chức năng nào được bao gồm.


1
Tôi có thể nhận được lời giải thích chi tiết về những gì xảy ra dưới mui xe trong đoạn 1 và 2? Tôi rất ngạc nhiên khi những thứ này có một cách khác để chạy vì tôi đang mong đợi các hành vi giống nhau.
Gregordy

2
@Gregordy vâng thật đáng ngạc nhiên. Tôi đã đăng câu trả lời này để lưu các lập trình viên mới để không đồng bộ một số vấn đề đau đầu. Đó là tất cả về khi JS đánh giá sự chờ đợi, đây là lý do tại sao cách bạn gán các biến quan trọng. Chuyên sâu Đọc Async: blog.bitsrc.io/ từ
GavinBelson

7

Bạn có thể tự kiểm tra.

Trong câu đố này , tôi đã chạy thử nghiệm để chứng minh bản chất ngăn chặn await, trái ngược với Promise.allđiều đó sẽ bắt đầu tất cả các lời hứa và trong khi một người đang chờ đợi thì nó sẽ tiếp tục với những người khác.


6
Trên thực tế, fiddle của bạn không giải quyết câu hỏi của mình. Có một sự khác biệt giữa gọi t1 = task1(); t2 = task2()sau đó sử dụng awaitcho cả hai người result1 = await t1; result2 = await t2;như trong câu hỏi của anh ấy, trái ngược với những gì bạn đang kiểm tra đang sử dụng awaittrên cuộc gọi ban đầu như thế nào result1 = await task1(); result2 = await task2();. Mã trong câu hỏi của anh ấy bắt đầu tất cả các lời hứa cùng một lúc. Sự khác biệt, giống như câu trả lời cho thấy, là những thất bại sẽ được báo cáo nhanh hơn Promise.alltheo cách này.
BryanGrezeszak

Câu trả lời của bạn không đúng chủ đề, chẳng hạn như @BryanGrezeszak đã nhận xét. Bạn nên xóa nó để tránh gây hiểu lầm cho người dùng.
mikep

0

Trong trường hợp đang chờ Promise.all ([task1 (), task2 ()]); "task1 ()" và "task2 ()" sẽ chạy song song và sẽ đợi cho đến khi cả hai lời hứa được hoàn thành (được giải quyết hoặc từ chối). Trong trường hợp

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

t2 sẽ chỉ chạy sau khi t1 thực hiện xong (đã được giải quyết hoặc từ chối). Cả t1 và t2 sẽ không chạy song song.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.