Pierwsza różnica - porażka szybko
Zgadzam się z odpowiedzią @ zzzzBov, ale przewaga Promise.all „szybko zawiedzie” to nie tylko jedna różnica. Niektórzy użytkownicy w komentarzach pytają, dlaczego używać Promise.all, gdy jest to szybsze tylko w negatywnym scenariuszu (gdy niektóre zadanie nie powiedzie się). I pytam dlaczego nie? Jeśli mam dwa niezależne, równoległe zadania asynchroniczne, a pierwsze z nich jest rozwiązywane w bardzo długim czasie, ale drugie jest odrzucane w bardzo krótkim czasie, dlaczego użytkownik ma czekać na komunikat o błędzie „bardzo długi czas” zamiast „bardzo krótki czas”? W rzeczywistych zastosowaniach musimy wziąć pod uwagę negatywny scenariusz. Ale OK - w tej pierwszej różnicy możesz zdecydować, która alternatywa dla Promise.all czy wielokrotnego czekania.
Druga różnica - obsługa błędów
Ale rozważając obsługę błędów, MUSISZ użyć Promise.all. Nie jest możliwe poprawne obsługiwanie błędów asynchronicznych zadań równoległych wyzwalanych z wieloma oczekiwaniami. W negatywnym scenariuszu zawsze skończysz UnhandledPromiseRejectionWarning
i PromiseRejectionHandledWarning
chociaż używasz try / catch wszędzie. Dlatego powstał Promise.all. Oczywiście ktoś mógłby powiedzieć, że możemy stłumić te błędy używając process.on('unhandledRejection', err => {})
i process.on('rejectionHandled', err => {})
ale to nie jest dobra praktyka. Znalazłem wiele przykładów w Internecie, które nie uwzględniają obsługi błędów dla dwóch lub więcej niezależnych równoległych zadań asynchronicznych w ogóle lub rozważają to w niewłaściwy sposób - po prostu używając try / catch i mając nadzieję, że wykryje błędy. Znalezienie dobrej praktyki jest prawie niemożliwe. Dlatego piszę tę odpowiedź.
Podsumowanie
Nigdy nie używaj wielokrotnego oczekiwania dla dwóch lub więcej niezależnych równoległych zadań asynchronicznych, ponieważ nie będziesz w stanie poważnie obsłużyć błędów. Zawsze używaj Promise.all () w tym przypadku użycia.
Async / await nie zastępuje obietnic. To po prostu ładny sposób, jak używać obietnic ... kod asynchroniczny jest napisany w stylu synchronizacji i możemy uniknąć wielu then
obietnic.
Niektórzy mówią, że używając Promise.all () nie możemy obsługiwać błędów zadań osobno, a jedynie błąd z pierwszej odrzuconej obietnicy (tak, niektóre przypadki użycia mogą wymagać oddzielnej obsługi np. Przy logowaniu). To nie jest problem - patrz nagłówek „Dodatek” poniżej.
Przykłady
Rozważ to zadanie asynchroniczne ...
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)
});
};
Kiedy uruchamiasz zadania w pozytywnym scenariuszu, nie ma różnicy między Promise.all a Multiple await. Oba przykłady kończą się Task 1 succeed! Task 2 succeed!
po 5 sekundach.
// 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!
Kiedy pierwsze zadanie zajmuje 10 sekund w scenariuszu pozytywnym, a zadanie sekundowe zajmuje 5 sekund w scenariuszu negatywnym, występują różnice w wyświetlanych błędach.
// 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!
Powinniśmy już zauważyć, że robimy coś złego, gdy używamy wielu czeków równolegle. Oczywiście, aby uniknąć błędów, powinniśmy sobie z tym poradzić! Spróbujmy...
// 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!
Jak widać, aby pomyślnie obsłużyć błąd, musimy dodać tylko jeden catch do run
funkcji, a kod z logiką catch jest w wywołaniu zwrotnym ( styl async ). Nie potrzebujemy obsługi błędów wewnątrz run
funkcji, ponieważ funkcja asynchroniczna robi to automatycznie - obietnica odrzucenia task
funkcji powoduje odrzucenie run
funkcji. Aby uniknąć wywołania zwrotnego, możemy użyć stylu synchronizacji (async / await + try / catch), try { await run(); } catch(err) { }
ale w tym przykładzie nie jest to możliwe, ponieważ nie możemy użyć await
w głównym wątku - można go używać tylko w funkcji async (jest to logiczne, ponieważ nikt nie chce blok główny wątek). Aby sprawdzić, czy obsługa działa w stylu synchronizacji , możemy wywołaćrun
Funkcja innej funkcji asynchronicznej lub użytkowania Iife (bezpośrednio Wykonano wyrażenie funkcyjne) (async function() { try { await run(); } catch(err) { console.log('Caught error', err); }; })();
.
To tylko jeden poprawny sposób uruchamiania dwóch lub więcej równoległych zadań asynchronicznych i obsługi błędów. Powinieneś unikać poniższych przykładów.
// 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);
};
Możemy spróbować obsłużyć powyższy kod na kilka sposobów ...
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
... nic nie zostało złapane, ponieważ obsługuje kod synchronizacji, ale run
jest asynchroniczne
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? Najpierw widzimy, że błąd dla zadania 2 nie został obsłużony, a później został przechwycony. Wprowadzające w błąd i wciąż pełne błędów w konsoli. Nie do użytku w ten sposób.
(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)
... tak samo jak powyżej. Użytkownik @Qwerty w swojej usuniętej odpowiedzi zapytał o to dziwne zachowanie, które wydaje się być przechwytywane, ale są też nieobsłużone błędy. Łapiemy błąd, ponieważ run () jest odrzucany w linii ze słowem kluczowym await i może zostać przechwycony przy użyciu try / catch podczas wywoływania run (). Otrzymujemy również nieobsłużony błąd, ponieważ wywołujemy funkcję zadania asynchronicznego synchronicznie (bez słowa kluczowego await), a to zadanie jest uruchamiane poza funkcją run () i również kończy się niepowodzeniem na zewnątrz. Jest podobny, gdy nie jesteśmy w stanie obsłużyć błąd przez try / catch podczas wywoływania niektórych funkcji synchronizacji, która część uruchamia kod w 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)
... „tylko” dwa błędy (brakuje trzeciego), ale nic nie złapano.
Dodawanie (osobno obsługuj błędy zadań, a także błąd pierwszego niepowodzenia)
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!
... zwróć uwagę, że w tym przykładzie użyłem negatywnegoScenario = true dla obu zadań, aby lepiej zademonstrować, co się dzieje ( throw err
służy do wywołania końcowego błędu)