Hallo Habr! Ich präsentiere Ihnen die Übersetzung des Artikels
„ Grundlegendes zu
asynchronem JavaScript“ von Sukhjinder Arora.
Vom Autor der Übersetzung: Ich hoffe, die Übersetzung dieses Artikels hilft Ihnen, sich mit etwas Neuem und Nützlichem vertraut zu machen. Wenn der Artikel Ihnen geholfen hat, seien Sie nicht faul und danken Sie dem Autor des Originals. Ich gebe nicht vor, ein professioneller Übersetzer zu sein, ich beginne gerade mit der Übersetzung von Artikeln und freue mich über jedes sinnvolle Feedback.JavaScript ist eine Single-Threaded-Programmiersprache, in der jeweils nur eine Sache ausgeführt werden kann. Das heißt, in einem einzelnen Thread kann die JavaScript-Engine jeweils nur eine Anweisung verarbeiten.
Single-Thread-Sprachen erleichtern zwar das Schreiben von Code, da Sie sich nicht um Parallelitätsprobleme kümmern müssen. Dies bedeutet jedoch auch, dass Sie keine langen Vorgänge wie den Zugriff auf das Netzwerk ausführen können, ohne den Haupt-Thread zu blockieren.
Senden Sie eine API-Anfrage für einige Daten. Abhängig von der Situation kann es einige Zeit dauern, bis der Server Ihre Anfrage bearbeitet, während die Ausführung des Hauptstroms blockiert wird, wodurch Ihre Webseite nicht mehr auf Anfragen reagiert.
Hier kommt die JavaScript-Asynchronität ins Spiel. Mithilfe der JavaScript-Asynchronität (Rückrufe, Versprechen und Async / Warten) können Sie lange Netzwerkanforderungen ausführen, ohne den Hauptthread zu blockieren.
Obwohl es nicht notwendig ist, alle diese Konzepte zu lernen, um ein guter JavaScript-Entwickler zu sein, ist es nützlich, sie zu kennen.
Also, ohne weiteres, fangen wir an.
Wie funktioniert synchrones Javascript?
Bevor wir uns mit der Arbeit von asynchronem JavaScript befassen, wollen wir zunächst verstehen, wie synchroner Code in der JavaScript-Engine ausgeführt wird. Zum Beispiel:
const second = () => { console.log('Hello there!'); } const first = () => { console.log('Hi there!'); second(); console.log('The End'); } first();
Um zu verstehen, wie der obige Code in der JavaScript-Engine ausgeführt wird, müssen wir das Konzept des Ausführungskontexts und des Aufrufstapels (auch als Ausführungsstapel bezeichnet) verstehen.
Ausführungskontext
Der Ausführungskontext ist ein abstraktes Konzept der Umgebung, in der Code ausgewertet und ausgeführt wird. Immer wenn Code in JavaScript ausgeführt wird, wird er im Kontext der Ausführung ausgeführt.
Der Funktionscode wird im Kontext der Funktionsausführung ausgeführt, und der globale Code wird wiederum im globalen Ausführungskontext ausgeführt. Jede Funktion hat ihren eigenen Ausführungskontext.
Stapel aufrufen
Ein Aufrufstapel ist ein Stapel mit einer LIFO-Struktur (Last in, First Out, first used), in der alle Ausführungskontexte gespeichert werden, die während der Codeausführung erstellt wurden.
JavaScript hat nur einen Aufrufstapel, da es sich um eine Single-Threaded-Programmiersprache handelt. Die LIFO-Struktur bedeutet, dass Elemente nur oben im Stapel hinzugefügt und entfernt werden können.
Kehren wir nun zum obigen Code-Snippet zurück und versuchen zu verstehen, wie die JavaScript-Engine dies ausführt.
const second = () => { console.log('Hello there!'); } const first = () => { console.log('Hi there!'); second(); console.log('The End'); } first();

Was ist hier passiert?
Als der Code ausgeführt wurde, wurde ein globaler Ausführungskontext erstellt (dargestellt als
main () ) und oben im Aufrufstapel hinzugefügt. Wenn der Aufruf der Funktion
first () auftritt, wird er auch oben im Stapel hinzugefügt.
Als nächstes wird
console.log ('Hi there!') Oben im Aufrufstapel platziert und nach der Ausführung vom Stapel entfernt. Danach rufen wir die Funktion
second () auf , sodass sie oben auf dem Stapel platziert wird.
console.log ('Hallo!') wird oben im Stapel hinzugefügt und nach Abschluss der Ausführung daraus entfernt. Die
zweite () Funktion ist abgeschlossen und wird ebenfalls vom Stapel entfernt.
console.log ('The End') wurde oben im Stapel hinzugefügt und am Ende entfernt. Danach wird die Funktion
first () beendet und ebenfalls vom Stapel entfernt.
Die Programmausführung wird beendet, sodass der globale Aufrufkontext (
main () ) vom Stapel entfernt wird.
Wie funktioniert asynchrones JavaScript?
Nachdem wir nun ein grundlegendes Verständnis des Aufrufstapels und der Funktionsweise von synchronem JavaScript haben, kehren wir zu asynchronem JavaScript zurück.
Was ist Blockieren?
Nehmen wir an, wir verarbeiten Bildverarbeitung oder Netzwerkanforderung synchron. Zum Beispiel:
const processImage = (image) => { console.log('Image processed'); } const networkRequest = (url) => { return someData; } const greeting = () => { console.log('Hello World'); } processImage(logo.jpg); networkRequest('www.somerandomurl.com'); greeting();
Bildverarbeitung und Netzwerkanforderung brauchen Zeit. Wenn die Funktion
processImage () aufgerufen wird,
dauert ihre Ausführung abhängig von der Größe des Bildes einige Zeit.
Wenn die Funktion
processImage () abgeschlossen ist, wird sie vom Stapel entfernt. Danach wird die Funktion
networkRequest () aufgerufen und dem Stack hinzugefügt. Dies wird wieder einige Zeit dauern, bevor die Ausführung abgeschlossen ist.
Wenn am Ende die Funktion
networkRequest () ausgeführt wird, wird die Funktion
greeting () aufgerufen, da sie nur die Methode
console.log enthält und diese Methode normalerweise schnell ausgeführt wird. Die Funktion
greeting () wird ausgeführt und sofort beendet.
Wie Sie sehen, müssen wir warten, bis die Funktion (z. B.
processImage () oder
networkRequest () ) abgeschlossen ist. Dies bedeutet, dass solche Funktionen den Aufrufstapel oder den Hauptthread blockieren. Infolgedessen können wir keine anderen Operationen ausführen, bis der obige Code ausgeführt wird.
Was ist die Lösung?
Die einfachste Lösung sind asynchrone Rückruffunktionen. Wir verwenden sie, um unseren Code nicht zu blockieren. Zum Beispiel:
const networkRequest = () => { setTimeout(() => { console.log('Async Code'); }, 2000); }; console.log('Hello World'); networkRequest();
Hier habe ich die
setTimeout- Methode verwendet, um eine Netzwerkanforderung zu simulieren. Bitte denken Sie daran, dass
setTimeout nicht Teil der JavaScript-Engine ist, sondern Teil der sogenannten Web-API (im Browser) und der C / C ++ - APIs (in node.js).
Um zu verstehen, wie dieser Code ausgeführt wird, müssen wir uns mit einigen weiteren Konzepten befassen, z. B. der Ereignisschleife und der Rückrufwarteschlange (auch als Taskwarteschlange oder Nachrichtenwarteschlange bezeichnet).

Die Ereignisschleife, die Web-API und die Nachrichtenwarteschlange / Aufgabenwarteschlange sind nicht Teil der JavaScript-Engine, sondern Teil der JavaScript-JavaScript-Laufzeit oder der JavaScript-Laufzeit in Nodejs (im Fall von Nodejs). In Nodejs werden Web-APIs durch C / C ++ - APIs ersetzt.
Kehren wir nun zum obigen Code zurück und sehen, was bei asynchroner Ausführung passiert.
const networkRequest = () => { setTimeout(() => { console.log('Async Code'); }, 2000); }; console.log('Hello World'); networkRequest(); console.log('The End');

Wenn der obige Code in den Browser geladen wird, wird
console.log ('Hello World') zum Stapel hinzugefügt und nach Abschluss der Ausführung von diesem entfernt. Als nächstes wird ein Aufruf der Funktion
networkRequest () gefunden , der oben im Stapel hinzugefügt wird.
Als nächstes wird die Funktion
setTimeout () aufgerufen und oben auf dem Stapel platziert. Die Funktion
setTimeout () hat zwei Argumente: 1) eine Rückruffunktion und 2) Zeit in Millisekunden.
setTimeout () startet einen Timer für 2 Sekunden in einer Web-API-Umgebung. Zu diesem Zeitpunkt wird
setTimeout () abgeschlossen und vom Stapel entfernt. Danach wird
console.log ('The End') zum Stack hinzugefügt, ausgeführt und nach Abschluss daraus entfernt.
Inzwischen ist der Timer abgelaufen, jetzt wird der Rückruf zur Nachrichtenwarteschlange hinzugefügt. Der Rückruf kann jedoch nicht sofort ausgeführt werden, und hier tritt der Ereignisverarbeitungszyklus in den Prozess ein.
Ereignisschleife
Die Aufgabe der Ereignisschleife besteht darin, den Aufrufstapel zu verfolgen und festzustellen, ob er leer ist oder nicht. Wenn der Aufrufstapel leer ist, prüft die Ereignisschleife in der Nachrichtenwarteschlange, ob Rückrufe vorhanden sind, die darauf warten, abgeschlossen zu werden.
In unserem Fall enthält die Nachrichtenwarteschlange einen Rückruf, und der Ausführungsstapel ist leer. Daher fügt die Ereignisschleife oben im Stapel einen Rückruf hinzu.
Nachdem
console.log ('Async Code') oben im Stapel hinzugefügt, ausgeführt und daraus entfernt wurde. Zu diesem Zeitpunkt ist der Rückruf abgeschlossen und vom Stapel entfernt, und das Programm ist vollständig abgeschlossen.
DOM-Ereignisse
Die Nachrichtenwarteschlange enthält auch Rückrufe von DOM-Ereignissen wie Klicks und Tastaturereignisse. Zum Beispiel:
document.querySelector('.btn').addEventListener('click',(event) => { console.log('Button Clicked'); });
Bei DOM-Ereignissen ist der Ereignishandler von der Web-API umgeben und wartet auf ein bestimmtes Ereignis (in diesem Fall einen Klick). Wenn dieses Ereignis auftritt, wird die Rückruffunktion in die Nachrichtenwarteschlange gestellt und wartet auf ihre Ausführung.
Wir haben gelernt, wie asynchrone Rückrufe und DOM-Ereignisse ausgeführt werden, die eine Nachrichtenwarteschlange verwenden, um Rückrufe zu speichern, die auf ihre Ausführung warten.
ES6 MicroTask-Warteschlange
Hinweis Übersetzungsautor: In dem Artikel hat der Autor die Nachrichten- / Aufgabenwarteschlange und die Job- / Micro-Taks-Warteschlange verwendet. Wenn Sie jedoch die Aufgabenwarteschlange und die Jobwarteschlange übersetzen, stellt sich theoretisch dasselbe heraus. Ich sprach mit dem Autor der Übersetzung und beschloss, das Konzept der Jobwarteschlange einfach wegzulassen. Wenn Sie irgendwelche Gedanken dazu haben, dann warte ich in den Kommentaren auf Sie
Link zur Übersetzung des Artikels durch Versprechen desselben Autors
ES6 führte das Konzept der Mikrotask-Warteschlange ein, die von Promises in JavaScript verwendet werden. Der Unterschied zwischen der Nachrichtenwarteschlange und der Mikrotask-Warteschlange besteht darin, dass die Mikrotask-Warteschlange eine höhere Priorität als die Nachrichtenwarteschlange hat. Dies bedeutet, dass „Versprechen“ in der Mikrotask-Warteschlange früher ausgeführt werden als Rückrufe in der Nachrichtenwarteschlange.
Zum Beispiel:
console.log('Script start'); setTimeout(() => { console.log('setTimeout'); }, 0); new Promise((resolve, reject) => { resolve('Promise resolved'); }).then(res => console.log(res)) .catch(err => console.log(err)); console.log('Script End');
Fazit:
Script start Script End Promise resolved setTimeout
Wie Sie sehen können, wurde das "Versprechen" vor
setTimeout ausgeführt , weil die Antwort des "Versprechens" in der Mikrostask-Warteschlange gespeichert ist, die eine höhere Priorität als die Nachrichtenwarteschlange hat.
Schauen wir uns das folgende Beispiel an, diesmal 2 "Versprechen" und 2
setTimeout :
console.log('Script start'); setTimeout(() => { console.log('setTimeout 1'); }, 0); setTimeout(() => { console.log('setTimeout 2'); }, 0); new Promise((resolve, reject) => { resolve('Promise 1 resolved'); }).then(res => console.log(res)) .catch(err => console.log(err)); new Promise((resolve, reject) => { resolve('Promise 2 resolved'); }).then(res => console.log(res)) .catch(err => console.log(err)); console.log('Script End');
Fazit:
Script start Script End Promise 1 resolved Promise 2 resolved setTimeout 1 setTimeout 2
Wiederum wurden unsere beiden „Versprechen“ vor den Rückrufen in
setTimeout ausgeführt , da die Ereignisverarbeitungsschleife Aufgaben aus der Mikrotask-Warteschlange für wichtiger hält als Aufgaben aus der Nachrichtenwarteschlange / Aufgabenwarteschlange.
Wenn während der Ausführung von Aufgaben ein weiteres "Versprechen" aus der Mikrotask-Warteschlange angezeigt wird, wird es am Ende dieser Warteschlange hinzugefügt und vor Rückrufen aus der Nachrichtenwarteschlange ausgeführt, unabhängig davon, wie lange sie auf ihre Ausführung warten.
Zum Beispiel:
console.log('Script start'); setTimeout(() => { console.log('setTimeout'); }, 0); new Promise((resolve, reject) => { resolve('Promise 1 resolved'); }).then(res => console.log(res)); new Promise((resolve, reject) => { resolve('Promise 2 resolved'); }).then(res => { console.log(res); return new Promise((resolve, reject) => { resolve('Promise 3 resolved'); }) }).then(res => console.log(res)); console.log('Script End');
Fazit:
Script start Script End Promise 1 resolved Promise 2 resolved Promise 3 resolved setTimeout
Somit werden alle Aufgaben aus der Mikrotask-Warteschlange vor Aufgaben aus der Nachrichtenwarteschlange abgeschlossen. Das heißt, die Ereignisverarbeitungsschleife löscht zuerst die Mikrotask-Warteschlange und beginnt erst dann, Rückrufe aus der Nachrichtenwarteschlange auszuführen.
Fazit
Wir haben also gelernt, wie asynchrones JavaScript funktioniert und Konzepte: Aufrufstapel, Ereignisschleife, Nachrichtenwarteschlange / Taskwarteschlange und Mikrotaskwarteschlange, aus denen die JavaScript-Laufzeit besteht