Asynchrones / wartendes JavaScript-Design: Stärken, Fallstricke und Verwendungsmuster

Das Async / Await-Konstrukt erschien im ES7-Standard. Dies kann als bemerkenswerte Verbesserung im Bereich der asynchronen Programmierung in JavaScript angesehen werden. Sie können Code schreiben, der synchron aussieht, aber zum Lösen asynchroner Aufgaben verwendet wird und den Hauptthread nicht blockiert. Trotz der Tatsache, dass Async / Warten eine großartige neue Funktion der Sprache ist, ist die korrekte Verwendung nicht so einfach. Das Material, dessen Übersetzung wir heute veröffentlichen, widmet sich einer umfassenden Untersuchung von Async / Warten und einer Geschichte darüber, wie dieser Mechanismus richtig und effektiv eingesetzt werden kann.

Bild

Stärken der Asynchronität / warten


Der wichtigste Vorteil eines Programmierers, der das Konstrukt async / await verwendet, besteht darin, dass es möglich ist, asynchronen Code in einem für synchronen Code typischen Stil zu schreiben. Vergleichen Sie den mit async / await geschriebenen Code mit dem auf Versprechungen basierenden Code.

// async/await async getBooksByAuthorWithAwait(authorId) {  const books = await bookModel.fetchAll();  return books.filter(b => b.authorId === authorId); } //  getBooksByAuthorWithPromise(authorId) {  return bookModel.fetchAll()    .then(books => books.filter(b => b.authorId === authorId)); } 

Es ist leicht zu bemerken, dass die asynchrone / wartende Version des Beispiels verständlicher ist als die Version, in der das Versprechen verwendet wird. Wenn Sie das Schlüsselwort await nicht beachten, sieht dieser Code wie ein regulärer Satz von Anweisungen aus, die synchron ausgeführt werden - wie in bekanntem JavaScript oder in einer anderen synchronen Sprache wie Python.

Die Attraktivität von async / await beruht nicht nur auf einer verbesserten Lesbarkeit des Codes. Dieser Mechanismus verfügt außerdem über eine hervorragende Browserunterstützung, für die keine Problemumgehungen erforderlich sind. Daher unterstützen asynchrone Funktionen heute alle gängigen Browser vollständig.


Alle gängigen Browser unterstützen asynchrone Funktionen ( caniuse.com )

Diese Unterstützungsstufe bedeutet beispielsweise, dass Code, der async / await verwendet, nicht transponiert werden muss . Darüber hinaus erleichtert es das Debuggen, was vielleicht sogar noch wichtiger ist als das Fehlen eines Transpilationsbedarfs.

Die folgende Abbildung zeigt den Debugging-Prozess einer asynchronen Funktion. Wenn Sie beim Festlegen eines Haltepunkts für die erste Anweisung der Funktion und beim Ausführen des Befehls "Step Over" die Zeile erreichen, in der das Schlüsselwort " await verwendet wird, können Sie feststellen, wie der Debugger eine Weile pausiert und auf den bookModel.fetchAll() Funktion " bookModel.fetchAll() wartet springt zu der Zeile, in der der Befehl .filter() ! Ein solcher Debugging-Prozess sieht viel einfacher aus als das Debuggen von Versprechungen. Hier müssten Sie beim Debuggen von ähnlichem Code einen anderen Haltepunkt in der .filter() .


Debuggen einer asynchronen Funktion. Der Debugger wartet, bis die Wartezeile abgeschlossen ist, und wechselt nach Abschluss des Vorgangs zur nächsten Zeile

Eine weitere Stärke des betrachteten Mechanismus, die weniger offensichtlich ist als das, was wir bereits untersucht haben, ist das Vorhandensein des async Schlüsselworts hier. In unserem Fall stellt seine Verwendung sicher, dass der von getBooksByAuthorWithAwait() Wert ein Versprechen ist. Infolgedessen können Sie das getBooksByAuthorWithAwait().then(...) sicher verwenden getBooksByAuthorWithAwait().then(...) oder await getBooksByAuthorWithAwait() Konstrukt await getBooksByAuthorWithAwait() im Code await getBooksByAuthorWithAwait() , der diese Funktion aufruft. Betrachten Sie das folgende Beispiel (beachten Sie, dass dies nicht empfohlen wird):

 getBooksByAuthorWithPromise(authorId) { if (!authorId) {   return null; } return bookModel.fetchAll()   .then(books => books.filter(b => b.authorId === authorId)); } } 

Hier kann die Funktion getBooksByAuthorWithPromise() , wenn alles in Ordnung ist, ein Versprechen zurückgeben oder, wenn etwas schief gelaufen ist, null . Wenn daher ein Fehler auftritt, können Sie .then() nicht sicher aufrufen. Beim Deklarieren von Funktionen mit dem async Fehler dieser Art nicht möglich.

Über die falsche Wahrnehmung von Async / Warten


In einigen Veröffentlichungen wird das Async / Await-Konstrukt mit Versprechungen verglichen und soll die nächste Generation der Entwicklung der asynchronen JavaScript-Programmierung darstellen. Bei allem Respekt vor den Autoren solcher Veröffentlichungen erlaube ich mir, dem nicht zuzustimmen. Async / await ist eine Verbesserung, aber nichts anderes als „syntaktischer Zucker“, dessen Erscheinungsbild nicht zu einer vollständigen Änderung des Programmierstils führt.

Asynchrone Funktionen sind im Wesentlichen Versprechen. Bevor ein Programmierer das asynchrone / warten-Konstrukt richtig verwenden kann, sollte er die Versprechen gut studieren. Darüber hinaus müssen Sie in den meisten Fällen beim Arbeiten mit asynchronen Funktionen Versprechen verwenden.

Schauen Sie sich die Funktionen getBooksByAuthorWithAwait() und getBooksByAuthorWithPromises() aus dem obigen Beispiel an. Bitte beachten Sie, dass sie nicht nur hinsichtlich der Funktionalität identisch sind. Sie haben auch genau die gleichen Schnittstellen.

All dies bedeutet, dass wenn Sie die Funktion getBooksByAuthorWithAwait() direkt aufrufen, das Versprechen zurückgegeben wird.

Tatsächlich ist das Wesentliche des Problems, über das wir hier sprechen, die falsche Wahrnehmung des neuen Designs, wenn es ein irreführendes Gefühl erzeugt, dass eine synchrone Funktion aufgrund der einfachen Verwendung der asynchronen Funktion in asynchron konvertiert werden kann und await Schlüsselwörter await und an nichts anderes denkt.

Fallstricke von Async / erwarten


Lassen Sie uns über die häufigsten Fehler sprechen, die mit async / await gemacht werden können. Insbesondere über die irrationale Verwendung aufeinanderfolgender Aufrufe asynchroner Funktionen.

Obwohl das Schlüsselwort await kann, dass der Code bei seiner Verwendung synchron aussieht, sollten Sie bedenken, dass der Code asynchron ist. Dies bedeutet, dass Sie beim sequentiellen Aufruf asynchroner Funktionen sehr vorsichtig sein müssen.

 async getBooksAndAuthor(authorId) { const books = await bookModel.fetchAll(); const author = await authorModel.fetch(authorId); return {   author,   books: books.filter(book => book.authorId === authorId), }; } 

Dieser Code scheint logisch korrekt zu sein. Es gibt jedoch ein ernstes Problem. So funktioniert es.

  1. Die Systemaufrufe await bookModel.fetchAll() und await bookModel.fetchAll() der Befehl .fetchAll() abgeschlossen ist.
  2. Nachdem Sie das Ergebnis von bookModel.fetchAll() await authorModel.fetch(authorId) aufgerufen wird.

Beachten Sie, dass der Aufruf von authorModel.fetch(authorId) unabhängig von den Ergebnissen des Aufrufs von bookModel.fetchAll() ist und diese beiden Befehle tatsächlich parallel ausgeführt werden können. Die Verwendung von await führt jedoch dazu, dass diese beiden Aufrufe nacheinander ausgeführt werden. Die Gesamtzeit der sequentiellen Ausführung dieser beiden Befehle ist länger als die Zeit ihrer parallelen Ausführung.

Hier ist der richtige Ansatz zum Schreiben eines solchen Codes:

 async getBooksAndAuthor(authorId) { const bookPromise = bookModel.fetchAll(); const authorPromise = authorModel.fetch(authorId); const book = await bookPromise; const author = await authorPromise; return {   author,   books: books.filter(book => book.authorId === authorId), }; } 

Betrachten Sie ein weiteres Beispiel für den Missbrauch asynchroner Funktionen. Dies ist immer noch schlimmer als im vorherigen Beispiel. Wie Sie sehen, müssen wir uns auf die Möglichkeiten von Versprechungen verlassen, um eine Liste bestimmter Elemente asynchron zu laden.

 async getAuthors(authorIds) { //  ,     // const authors = _.map( //   authorIds, //   id => await authorModel.fetch(id)); //   const promises = _.map(authorIds, id => authorModel.fetch(id)); const authors = await Promise.all(promises); } 

Kurz gesagt, um asynchrone Funktionen korrekt zu verwenden, müssen Sie, wie zu einem Zeitpunkt, als dies nicht möglich war, zuerst über asynchrone Operationen nachdenken und dann Code mit await schreiben. In komplexen Fällen wird es wahrscheinlich einfacher sein, Versprechen direkt zu verwenden.

Fehlerbehandlung


Wenn Sie Versprechen verwenden, kann die Ausführung von asynchronem Code entweder wie erwartet enden - dann sagen sie, dass das Versprechen erfolgreich gelöst wurde, oder mit einem Fehler - dann sagen sie, dass das Versprechen abgelehnt wird. Dies ermöglicht es uns, .then() bzw. .catch() verwenden. Die Fehlerbehandlung mit dem Async / Wait-Mechanismus kann jedoch schwierig sein.

▍ try / catch-Konstrukt


Die Standardmethode zur Behandlung von Fehlern bei Verwendung von async / await ist das try / catch-Konstrukt. Ich empfehle diesen Ansatz. Bei einem Warteanruf wird der Wert, der zurückgegeben wird, wenn das Versprechen abgelehnt wird, als Ausnahme angezeigt. Hier ist ein Beispiel:

 class BookModel { fetchAll() {   return new Promise((resolve, reject) => {     window.setTimeout(() => { reject({'error': 400}) }, 1000);   }); } } // async/await async getBooksByAuthorWithAwait(authorId) { try { const books = await bookModel.fetchAll(); } catch (error) { console.log(error);    // { "error": 400 } } 

Der im catch abgefangene Fehler ist genau der Wert, der erhalten wird, wenn das Versprechen abgelehnt wird. Nachdem wir eine Ausnahme abgefangen haben, können wir verschiedene Ansätze anwenden, um damit zu arbeiten:

  • Sie können die Ausnahme behandeln und den normalen Wert zurückgeben. Wenn Sie den return Ausdruck im catch nicht verwenden, um das return , was nach Ausführung der asynchronen Funktion erwartet wird, entspricht dies der Verwendung des Befehls return undefined ;
  • Sie können den Fehler einfach an die Stelle übergeben, an der der fehlgeschlagene Code aufgerufen wurde, und ihn dort verarbeiten lassen. Sie können einen Fehler direkt auslösen, indem Sie einen Befehl wie throw error; async getBooksByAuthorWithAwait() können Sie die async getBooksByAuthorWithAwait() Funktion async getBooksByAuthorWithAwait() in der Kette der Versprechen verwenden. Das heißt, es kann mit dem getBooksByAuthorWithAwait().then(...).catch(error => ...) aufgerufen werden. Darüber hinaus können Sie den Fehler in ein Error einschließen, das wie ein throw new Error(error) aussehen kann. Auf diese Weise können Sie beispielsweise bei der Ausgabe von Fehlerinformationen an die Konsole den vollständigen Aufrufstapel anzeigen.
  • Der Fehler kann als abgelehntes Versprechen dargestellt werden. Es sieht aus wie return Promise.reject(error) . In diesem Fall entspricht dies dem Befehl throw error Dies wird nicht empfohlen.

Hier sind die Vorteile der Verwendung des try / catch-Konstrukts:

  • Solche Fehlerbehandlungswerkzeuge gibt es in der Programmierung schon sehr lange, sie sind einfach und verständlich. Wenn Sie Programmiererfahrung in anderen Sprachen wie C ++ oder Java haben, werden Sie das Try / Catch-Gerät in JavaScript leicht verstehen.
  • Sie können mehrere Warte-Aufrufe in einem Try / Catch-Block platzieren, sodass Sie alle Fehler an einem Ort behandeln können, wenn Sie Fehler nicht bei jedem Schritt der Codeausführung separat behandeln müssen.

Es ist zu beachten, dass der Try / Catch-Mechanismus einen Nachteil aufweist. Da try / catch alle Ausnahmen abfängt, die im try Block auftreten, werden auch die Ausnahmen, die nicht mit Versprechungen zusammenhängen, in den catch Handler aufgenommen. Schauen Sie sich dieses Beispiel an.

 class BookModel { fetchAll() {   cb();    //    ,   `cb`  ,       return fetch('/books'); } } try { bookModel.fetchAll(); } catch(error) { console.log(error);  //       "cb is not defined" } 

Wenn Sie diesen Code ausführen, wird in der Konsole die ReferenceError: cb is not defined angezeigt. Diese Nachricht wird vom Befehl console.log() aus dem catch und nicht von JavaScript selbst ausgegeben. In einigen Fällen führen solche Fehler zu schwerwiegenden Konsequenzen. Zum Beispiel, wenn Sie bookModel.fetchAll(); aufrufen bookModel.fetchAll(); Tief in einer Reihe von Funktionsaufrufen versteckt und einer der Aufrufe wird einen Fehler "verschlucken", wird es sehr schwierig sein, einen solchen Fehler zu erkennen.

▍ Funktionsrückgabe zweier Werte


Die Inspiration für den nächsten Weg, um Fehler im asynchronen Code zu behandeln, ist Go. Es ermöglicht asynchronen Funktionen, sowohl einen Fehler als auch ein Ergebnis zurückzugeben. Lesen Sie hier mehr darüber.

Kurz gesagt, dann können asynchrone Funktionen mit diesem Ansatz wie folgt verwendet werden:

 [err, user] = await to(UserModel.findById(1)); 

Persönlich gefällt mir das nicht, weil diese Fehlerbehandlungsmethode den Go-Programmierstil in JavaScript einführt, was unnatürlich aussieht, obwohl es in einigen Fällen sehr nützlich sein kann.

▍Verwendung von .catch


Die letzte Möglichkeit, mit Fehlern .catch() , über die wir sprechen werden, ist die Verwendung von .catch() .

Überlegen Sie, wie das await funktioniert. Die Verwendung dieses Schlüsselworts führt nämlich dazu, dass das System wartet, bis das Versprechen seine Arbeit abgeschlossen hat. promise.catch() auch daran, dass ein Befehl des Formulars promise.catch() auch ein Versprechen zurückgibt. All dies deutet darauf hin, dass asynchrone Funktionsfehler folgendermaßen behandelt werden können:

 // books   undefined   , //    catch     let books = await bookModel.fetchAll() .catch((error) => { console.log(error); }); 

Zwei kleine Probleme sind charakteristisch für diesen Ansatz:

  • Dies ist eine Mischung aus Versprechungen und asynchronen Funktionen. Um dies nutzen zu können, ist es wie in anderen ähnlichen Fällen notwendig, die Merkmale der Arbeit von Versprechungen zu verstehen.
  • Dieser Ansatz ist nicht intuitiv, da die Fehlerbehandlung an einem ungewöhnlichen Ort durchgeführt wird.

Zusammenfassung


Das in ES7 eingeführte async / await-Konstrukt ist definitiv eine Verbesserung der asynchronen JavaScript-Programmiermechanismen. Dies kann das Lesen und Debuggen von Code erleichtern. Um async / await richtig zu verwenden, ist jedoch ein tiefes Verständnis der Versprechen erforderlich, da async / await nur „syntaktischer Zucker“ ist, der auf Versprechen basiert.

Wir hoffen, dass Sie sich mit diesem Material mit Async / Warten vertraut gemacht haben. Was Sie hier gelernt haben, erspart Ihnen einige häufige Fehler, die bei der Verwendung dieser Konstruktion auftreten.

Liebe Leser! Verwenden Sie das Konstrukt async / await in JavaScript? Wenn ja, teilen Sie uns bitte mit, wie Sie mit Fehlern im asynchronen Code umgehen.

Source: https://habr.com/ru/post/de414373/


All Articles