So schreiben Sie asynchrone Aufgaben in modernem JavaScript

So schreiben Sie asynchrone Aufgaben in modernem JavaScript

Vorwort

In diesem Artikel untersuchen wir die Entwicklung von asynchronem JavaScript und wie es die Art und Weise verändert, wie wir Code schreiben. Wir beginnen mit den Anfängen der Webentwicklung und arbeiten uns bis zu modernen asynchronen Mustern vor.

Als Programmiersprache verfügt JavaScript über zwei Hauptmerkmale, die für das Verständnis der Funktionsweise unseres Codes sehr wichtig sind. Der erste Grund ist seine synchrone Natur, was bedeutet, dass der Code Zeile für Zeile ausgeführt wird. Der zweite Grund ist seine Single-Thread-Natur, wobei immer nur ein Befehl ausgeführt wird.

Im Zuge der Weiterentwicklung der Sprache entstehen neue Artefakte, die eine asynchrone Ausführung ermöglichen. Entwickler probierten unterschiedliche Ansätze beim Lösen komplexerer Algorithmen und Datenflüsse aus, was zu neuen Schnittstellen und Mustern führte.

Synchrone Ausführung und das Observer-Muster

Wie in der Einleitung erwähnt, führt JavaScript den von Ihnen geschriebenen Code normalerweise Zeile für Zeile aus. Schon in den ersten Jahren gab es in der Sprache Ausnahmen von dieser Regel, wenn auch nur wenige, und Sie kennen sie wahrscheinlich bereits: HTTP-Anfragen, DOM-Ereignisse und Zeitintervalle.

Wenn wir auf einen Benutzerklick auf ein Element reagieren, indem wir einen Ereignis-Listener hinzufügen, wird der gerade ausgeführte Sprachinterpreter angehalten und führt anschließend den im Listener-Callback geschriebenen Code aus, bevor zum normalen Ablauf zurückgekehrt wird.

Wie Intervalle oder Netzwerkanforderungen sind addEventListener, setTimeout und XMLHttpRequest die ersten Artefakte, bei denen Webentwickler Zugriff auf die asynchrone Ausführung haben.

Obwohl dies Ausnahmen von der synchronen Ausführung in JavaScript sind, ist es wichtig zu verstehen, dass die Sprache immer noch ein Thread ist. Wir können diese Synchronisierung unterbrechen, aber der Interpreter führt den Code trotzdem zeilenweise aus.

Beispielsweise die Überprüfung einer Netzwerkanforderung.

var request = neue XMLHttpRequest();
request.open('GET', '//some.api.at/server', true);

// Beobachten Sie die Serverantwort
Anfrage.onreadystatechange = Funktion() {
 wenn (request.readyState === 4 und xhr.status === 200) {
 Konsole.log(Anfrage.Antworttext);
 }
}

11Anfrage.senden();

Unabhängig davon, was passiert, wird beim Wiederherstellen des Servers die „onreadystatechange“ zugewiesene Methode vor der Codefolge des Fetchers aufgerufen.

Eine ähnliche Situation tritt beim Reagieren auf Benutzerinteraktionen auf.

const button = document.querySelector('button');

// auf Benutzerinteraktion achten
button.addEventListener('klicken', Funktion(e) {
 console.log('Benutzerklick ist gerade passiert!');
})

Möglicherweise bemerken Sie, dass wir eine Verbindung zu einem externen Ereignis herstellen und einen Rückruf übergeben, um unserem Code mitzuteilen, was zu tun ist, wenn das Ereignis eintritt. Vor über einem Jahrzehnt war „Was ist ein Rückruf?“ eine mit Spannung erwartete Interviewfrage, da dieses Muster in vielen Codebasen allgegenwärtig war.

In jedem dieser Fälle reagieren wir auf externe Ereignisse. Sei es das Erreichen eines bestimmten Zeitintervalls, eine Benutzeraktion oder eine Serverantwort. Wir können selbst keine asynchronen Aufgaben erstellen, wir beobachten immer Ereignisse, die außerhalb unseres Bereichs stattfinden.

Aus diesem Grund wird dieser Codierungsstil als Beobachtermuster bezeichnet und in diesem Fall am besten durch die Schnittstelle „addEventListener“ dargestellt. Bald begannen Ereignisemitter-Bibliotheken oder Frameworks aufzublühen, die dieses Muster offenlegten.

NODE.JS und Ereignisemitter

Node.js ist ein gutes Beispiel; die offizielle Website beschreibt sich selbst als „asynchrone ereignisgesteuerte JavaScript-Laufzeitumgebung“, sodass Ereignisemitter und Rückrufe erstklassige Eigenschaften haben. Es ist sogar ein EventEmitter-Konstruktor implementiert.

const EventEmitter = erfordern('Ereignisse');
const emitter = neuer EventEmitter();

// auf Ereignisse reagieren
emitter.on('Begrüßung', (Nachricht) => console.log(Nachricht));

// Ereignisse senden
emitter.emit('Begrüßung', 'Hallo zusammen!');

Dies ist nicht nur eine allgemeine Methode der asynchronen Ausführung, sondern auch ein Kernmuster und eine Konvention seines Ökosystems. Node.js hat eine neue Ära des Schreibens von JavaScript in verschiedenen Umgebungen eingeläutet, sogar außerhalb des Webs. Natürlich sind auch asynchrone Situationen möglich, wie etwa das Anlegen neuer Verzeichnisse oder das Schreiben von Dateien.

const { mkdir, writeFile } = require('fs');

const Stile = 'Body { Hintergrund: #ffdead; }';

mkdir('./assets/', (Fehler) => {
 wenn (!Fehler) {
 writeFile('assets/main.css', Stile, 'utf-8', (Fehler) => {
  if (!error) console.log('Stylesheet erstellt');
 })
 }
})

Möglicherweise stellen Sie fest, dass die Rückruffunktion den Fehler als erstes Argument erhält und, falls erwartete Antwortdaten vorlagen, diese als zweites Argument. Dies wird als „Error-First-Callback-Muster“ bezeichnet und wurde zu einer Konvention, die Autoren und Mitwirkende für Pakete und Bibliotheken entwickelten.

Versprechen und endlose Rückrufketten

Da die Webentwicklung mit komplexeren Problemen konfrontiert wird, entsteht der Bedarf an besseren asynchronen Artefakten. Wenn wir uns den letzten Codeausschnitt ansehen, können wir eine wiederholte Rückrufkette erkennen, die bei zunehmender Anzahl von Aufgaben nicht gut skaliert.

Beispielsweise fügen wir nur zwei Schritte hinzu: Lesen der Datei und Vorverarbeitung des Stils.

const { mkdir, Datei schreiben, Datei lesen } = require('fs');
const weniger = erfordern('weniger')

readFile('./main.less', 'utf-8', (Fehler, Daten) => {
 wenn (Fehler) Fehler auslösen
 weniger.render(Daten, (wenigerFehler, Ausgabe) => {
 wenn (wenigerFehler) wenigerFehler werfen
 mkdir('./assets/', (dirError) => {
  if (dirError) throw dirError
  schreibeDatei('assets/main.css', ausgabe.css, 'utf-8', (schreibeFehler) => {
  if (Schreibfehler) throw Schreibfehler
  console.log('Stylesheet erstellt');
  })
 })
 })
16})

Wir sehen, dass das Schreiben von Programmen zunehmend komplexer wird und der Code aufgrund mehrerer Callback-Ketten und wiederholter Fehlerbehandlungen immer schwerer zu verstehen ist.

Versprechen, Wrapper und Kettenmuster

Als Promises erstmals als neue Ergänzung der JavaScript-Sprache angekündigt wurden, erregten sie nicht viel Aufmerksamkeit. Sie sind kein neues Konzept, da andere Sprachen seit Jahrzehnten ähnliche Implementierungen haben. Tatsächlich haben sie die Semantik und Struktur der meisten Projekte verändert, an denen ich seit ihrem Erscheinen gearbeitet habe.

Promises führten nicht nur eine integrierte Lösung für Entwickler zum Schreiben asynchronen Codes ein, sondern leiteten auch eine neue Phase der Webentwicklung ein und wurden zur Grundlage, auf der später neue Funktionen der Web-Spezifikation, wie z. B. Fetch, aufgebaut wurden.

Die Migration vom Callback-Ansatz zum Promise-basierten Ansatz wird in Projekten wie Bibliotheken und Browsern immer üblicher, sogar Node.js beginnt langsam, dorthin zu migrieren.

Beispielsweise das Umschließen der readFile-Methode von Node:

const { readFile } = erfordern('fs');

const asyncReadFile = (Pfad, Optionen) => {
 returniere neues Promise((lösen, ablehnen) => {
  readFile(Pfad, Optionen, (Fehler, Daten) => {
   wenn (Fehler) ablehnen(Fehler);
   sonst auflösen (Daten);
  })
 });
}

Hier verbergen wir den Rückruf, indem wir ihn innerhalb des Promise-Konstruktors ausführen, bei Erfolg „resolve“ aufrufen und „reject“ mit einem definierten Fehlerobjekt aufrufen.

Wenn eine Methode ein Promise-Objekt zurückgibt, können wir dessen erfolgreiche Auflösung verfolgen, indem wir an then eine Funktion übergeben, deren Argument der Wert des aufzulösenden Promise ist, in diesem Fall also Daten.

Wenn während der Methode ein Fehler auftritt, wird die Catch-Funktion (sofern vorhanden) aufgerufen.

Hinweis: Wenn Sie tiefere Einblicke in die Funktionsweise von Promises benötigen, empfehle ich Ihnen den Artikel „JavaScript Promises: Eine Einführung“ von Jake Archibald im Webentwicklungsblog von Google.

Jetzt können wir diese neuen Methoden verwenden und Rückrufketten vermeiden.

asyncRead('./main.less', 'utf-8')
 .then(data => console.log('Dateiinhalt', data))
 .catch(error => console.error('etwas ist schiefgelaufen', Fehler))

Es verfügt über native Methoden zum Erstellen asynchroner Aufgaben und zum Verfolgen ihrer möglichen Ergebnisse mit einer klaren Schnittstelle, die das Beobachtermuster beseitigt. Promise-basierter Code scheint eine Lösung für schlecht lesbaren und fehleranfälligen Code zu sein.

Durch eine bessere Syntaxhervorhebung und eindeutigere Fehlermeldungen wird der Codierungsprozess unterstützt. So können Entwickler vorhersehbareren Code schreiben, der leichter zu verstehen ist und eine bessere Leistung bietet. So können mögliche Fallstricke leichter erkannt werden.

Die Einführung von Promises war in der Community so weit verbreitet, dass Node.js schnell integrierte Versionen seiner I/O-Methoden veröffentlichte, um Promise-Objekte zurückzugeben, wie z. B. das Importieren von Dateivorgängen aus fs.promises.

Es bietet sogar ein Promisify-Tool zum Umschließen von Funktionen, die dem Fehler-zuerst-Rückrufmuster folgen, und zum Konvertieren dieser in Promise-basierte Funktionen.

Aber helfen Versprechen in allen Fällen?

Lassen Sie uns unsere mit Promises geschriebene Aufgabe zur Stilvorverarbeitung neu bewerten.

const { mkdir, Datei schreiben, Datei lesen } = require('fs').promises;
const weniger = erfordern('weniger')

Datei lesen('./main.less', 'utf-8')
 .dann(weniger.render)
 .dann(Ergebnis =>
  mkdir('./Vermögenswerte')
   .dann(writeFile('assets/main.css', result.css, 'utf-8'))
 )
 .catch(Fehler => Konsole.Fehler(Fehler))

Da wir uns nun auf „Catch“ verlassen, ist der Code offensichtlich weniger ausführlich, insbesondere im Hinblick auf die Fehlerbehandlung. Allerdings gelingt es Promises nicht, eine klare Einrückung des Codes bereitzustellen, der in direktem Zusammenhang mit der Verkettung von Aktionen steht.

Tatsächlich wird dies in der ersten then-Anweisung nach dem Aufruf von readFile implementiert. Was nach diesen Codezeilen passiert, ist, dass ein neuer Bereich erstellt wird, in dem wir zuerst das Verzeichnis erstellen und dann das Ergebnis in die Datei schreiben können. Dadurch wird der Rhythmus der Einrückung unterbrochen, so dass die Reihenfolge der Anweisungen auf den ersten Blick schwer zu erkennen ist.

HINWEIS: Bitte beachten Sie, dass dies ein Beispielprogramm ist und wir die Kontrolle über bestimmte Methoden haben. Sie folgen alle der Branchenpraxis, was jedoch nicht immer der Fall sein muss. Unser Codierstil kann durch komplexere Verkettung oder die Einführung verschiedener Bibliotheken leicht aufgebrochen werden.

Glücklicherweise hat die JavaScript-Community auch dieses Mal wieder von der Syntax anderer Sprachen gelernt und eine Notation hinzugefügt, mit deren Hilfe sich asynchrone Aufgaben in den meisten Fällen aneinanderreihen lassen, ohne dabei so leicht lesbar zu sein wie synchroner Code.

Async und Await

Ein Promise wird bei der Ausführung als nicht aufgelöster Wert definiert und das Erstellen einer Promise-Instanz ist ein „expliziter“ Aufruf dieses Artefakts.

const { mkdir, Datei schreiben, Datei lesen } = require('fs').promises;
const weniger = erfordern('weniger')

Datei lesen('./main.less', 'utf-8')
 .dann(weniger.render)
 .dann(Ergebnis =>
  mkdir('./Vermögenswerte')
   .dann(writeFile('assets/main.css', result.css, 'utf-8'))
 )
 .catch(Fehler => Konsole.Fehler(Fehler))

Innerhalb einer asynchronen Methode können wir das reservierte Wort „await“ verwenden, um die Auflösung des Promise zu bestimmen, bevor wir mit der Ausführung fortfahren.

Lassen Sie uns den Codeausschnitt mit dieser Syntax neu schreiben.

const { mkdir, Datei schreiben, Datei lesen } = require('fs').promises;
const weniger = erfordern('weniger')

asynchrone Funktion processLess() {
 const-Inhalt = warte auf ReadFile('./main.less', 'utf-8').
 const Ergebnis = warte auf weniger.render(Inhalt)
 warte auf mkdir('./assets')
 warte auf writeFile('assets/main.css', result.css, 'utf-8')
}

11processLess()

Hinweis: Beachten Sie, dass wir unseren gesamten Code in eine Methode verschieben müssen, da wir „await“ nicht außerhalb des Gültigkeitsbereichs einer asynchronen Funktion verwenden können.

Wenn eine asynchrone Methode eine „Await“-Anweisung findet, wird ihre Ausführung gestoppt, bis das Versprechen eingelöst ist.

Obwohl der Code asynchron ausgeführt wird, lässt die Verwendung von „async/await“ den Code synchron aussehen und ist für Entwickler leicht zu lesen und zu verstehen.

Was ist mit der Fehlerbehandlung? Wir können Try und Catch verwenden, die es in der Sprache schon lange gibt.

const { mkdir, Datei schreiben, Datei lesen } = require('fs').promises;
const weniger = erfordern('weniger')

asynchrone Funktion processLess() {
 const-Inhalt = warte auf ReadFile('./main.less', 'utf-8').
 const Ergebnis = warte auf weniger.render(Inhalt)
 warte auf mkdir('./assets')
 warte auf writeFile('assets/main.css', result.css, 'utf-8')
}

versuchen {
 processLess()
} fangen (e) {
 konsole.fehler(e)
}

Wir können sicher sein, dass alle während des Verfahrens auftretenden Fehler durch den Code in der Catch-Anweisung behandelt werden. Jetzt haben wir einen lesbaren und klaren Code.

Nachfolgende Operationen am Rückgabewert müssen nicht in einer Variablen wie mkdir gespeichert werden, die den Rhythmus des Codes nicht stört; ebenso wenig muss ein neuer Bereich erstellt werden, um in späteren Schritten auf den Ergebniswert zuzugreifen.

Man kann mit Sicherheit sagen, dass Promises ein grundlegendes Artefakt sind, das in die Sprache eingeführt wurde und notwendig ist, um die async/await-Notation in JavaScript zu aktivieren, die Sie in modernen Browsern und den neuesten Versionen von Node.js verwenden können.

HINWEIS: Ryan Dahl, Entwickler und erster Mitwirkender bei Node, drückte kürzlich auf der JSConf sein Bedauern darüber aus, dass er sich in der frühen Entwicklung nicht an Promises gehalten hatte, hauptsächlich weil das Ziel von Node darin bestand, ereignisgesteuerte Server und Dateimanager zu erstellen, für die das Observer-Muster besser geeignet war.

abschließend

Mit der Einführung von Promises in die Webentwicklung sollte die Art und Weise geändert werden, wie wir Vorgänge in unserem Code sequenzieren. Außerdem veränderte sich dadurch die Art und Weise, wie wir Code verstehen und wie wir Bibliotheken und Pakete schreiben.

Das Entfernen von Callback-Ketten ist jedoch schwieriger und ich denke, dass uns die Notwendigkeit, Methoden an then zu übergeben, nicht dabei hilft, sie loszuwerden, nachdem wir uns jahrelang an das Observer-Muster und die übernommenen Ansätze wie Node.js gewöhnt haben.

Wie Nolan Lawson in seinem hervorragenden Artikel „Wir haben ein Problem mit kaskadierenden Promises“ erklärt, sind alte Callback-Gewohnheiten ein hartnäckiger Prozess! Darin erklärt er, wie man diese Fallstricke vermeidet.

Ich denke, dass Promises ein Zwischenschritt sind, der die Generierung asynchroner Aufgaben auf natürliche Weise ermöglicht, uns jedoch nicht dabei hilft, in Richtung besserer Codemuster voranzukommen, und dass man sich manchmal mit einer verbesserten Sprachsyntax vertrauter machen muss.

Als wir versuchten, komplexere Probleme mit JavaScript zu lösen, erkannten wir die Notwendigkeit einer ausgereifteren Sprache und experimentierten mit Architekturen und Mustern, die wir zuvor online nicht gesehen hatten.

Wir wissen noch nicht, wie die ECMAScript-Spezifikation in einigen Jahren aussehen wird, da wir die JavaScript-Governance über das Web hinaus immer weiter ausbauen und versuchen, komplexere Rätsel zu lösen.

Es ist derzeit schwer zu sagen, was wir von der Sprache brauchen, um diese schwierigen Probleme wirklich in einfachere Programme umzuwandeln, aber ich bin zufrieden damit, wie das Web und JavaScript selbst die Technologie vorantreiben und versuchen, sich an Herausforderungen und neue Umgebungen anzupassen. Ich habe das Gefühl, dass JavaScript heute viel „asynchronitätsfreundlicher“ ist als vor zehn Jahren, als ich anfing, Code im Browser zu schreiben.

Originalartikel: https://www.smashingmagazine.com/2019/10/asynchronous-tasks-modern-javascript/

Damit ist dieser Artikel zum Schreiben asynchroner Aufgaben in modernem JavaScript abgeschlossen. Weitere Informationen zum Schreiben asynchroner Aufgaben in JavaScript finden Sie in früheren Artikeln auf 123WORDPRESS.COM oder in den folgenden verwandten Artikeln. Ich hoffe, Sie werden 123WORDPRESS.COM auch in Zukunft unterstützen!

Das könnte Sie auch interessieren:
  • Detaillierte Erläuterung der asynchronen Prozessimplementierung in Single-Thread-JavaScript
  • Analysieren Sie die Eigenschaften des asynchronen IO-Rückrufs mit einem Thread in JS
  • Asynchrone Programmierung in Javascript: Verstehen Sie Promise wirklich?
  • Detaillierte Erläuterung der ersten Verwendung von Promise in der asynchronen JavaScript-Programmierung
  • JS-Prinzip der asynchronen Ausführung und Rückrufdetails
  • Detaillierte Erklärung asynchroner Generatoren und asynchroner Iterationen in Node.js
  • Lernen Sie die asynchrone Programmierung in Node.js in einem Artikel
  • Detaillierte Erläuterung der Wissenspunkte zur asynchronen Programmierung in nodejs
  • Eine kurze Diskussion über die drei Hauptprobleme von JS: Asynchronität und Single-Thread

<<:  So mounten Sie die Datenfestplatte nach dem Initialisieren der Systemfestplatte in Linux erneut

>>:  Centos7 installiert mysql5.6.29 Shell-Skript

Artikel empfehlen

Flex-Anordnung in CSS darstellen (Layouttool)

In Bezug auf die Anzeige: flexibles Layout: Manch...

Spezifische Verwendung von pthread_create in Linux zum Erstellen von Threads

pthread_create-Funktion Funktionseinführung pthre...

Eingabetyp begrenzen (mehrere Methoden)

1. Es können nur chinesische Schriftzeichen eingeg...

So führen Sie den Betrieb nach dem Verlassen des Docker-Containers weiter aus

Phänomen: Führen Sie ein Image aus, zum Beispiel ...

Die 7 besten VSCode-Erweiterungen für Vue-Entwickler

Das Hinzufügen der richtigen VS Code-Erweiterung ...

Bei der anonymen Mysql-Anmeldung kann keine Datenbankproblemlösung erstellt werden

Häufig gestellte Fragen Der Zugriff für den Benut...

So installieren Sie den Kibana-Tokenizer im Docker-Container

Schritt: 1. Erstellen Sie eine neue Datei docker-...

Einige Datenverarbeitungsmethoden, die häufig in JS verwendet werden können

Inhaltsverzeichnis DOM-Verarbeitung Arrays Verfah...

Gogs+Jenkins+Docker automatisierte Bereitstellung von .NetCore-Schritten

Inhaltsverzeichnis Umgebungsbeschreibung Docker-I...

Beispiel für das Hinzufügen von Attributen mithilfe von Stilen in HTML

Fügen Sie den erforderlichen Links Inline-Stile hi...

Vue implementiert ein verschiebbares Baumstrukturdiagramm

Inhaltsverzeichnis Rekursive Vue-Komponente Drag-...

Spezifische Verwendung von Linux which Befehl

Oft möchten wir in Linux eine Datei finden, wisse...

MySQL 8.0.16 Installations- und Konfigurations-Tutorial unter CentOS7

Deinstallieren Sie die alte MySQL-Version (übersp...

Lösung für das Problem der Zeilenhöhe der Elementtabellenkopfzeile

Inhaltsverzeichnis Vorwort 1. Ursache des Problem...