Bonjour, Habr! Je vous présente la traduction de l'article
«Understanding Asynchronous JavaScript» de Sukhjinder Arora.
De l'auteur de la traduction: J'espère que la traduction de cet article vous aidera à vous familiariser avec quelque chose de nouveau et d'utile. Si l'article vous a aidé, alors ne soyez pas paresseux et remerciez l'auteur de l'original. Je ne prétends pas être un traducteur professionnel, je commence tout juste à traduire des articles et je serai heureux de tout retour d'information.JavaScript est un langage de programmation à thread unique dans lequel une seule chose peut être exécutée à la fois. Autrement dit, dans un seul thread, le moteur JavaScript ne peut traiter qu'une seule instruction à la fois.
Bien que les langues à un seul thread facilitent l'écriture de code, puisque vous n'avez pas à vous soucier des problèmes de concurrence, cela signifie également que vous ne pourrez pas effectuer de longues opérations telles que l'accès au réseau sans bloquer le thread principal.
Soumettez une demande d'API pour certaines données. Selon la situation, le serveur peut prendre un certain temps pour traiter votre demande, tandis que l'exécution du flux principal sera bloquée en raison de laquelle votre page Web cessera de répondre aux demandes.
C'est là que l'asynchronie JavaScript entre en jeu. En utilisant l'asynchronie JavaScript (rappels, promesses et asynchronisation / attente), vous pouvez effectuer de longues requêtes réseau sans bloquer le thread principal.
Bien qu'il ne soit pas nécessaire d'apprendre tous ces concepts pour être un bon développeur JavaScript, il est utile de les connaître.
Alors, sans plus tarder, commençons.
Comment fonctionne le javascript synchrone?
Avant de nous plonger dans le travail du JavaScript asynchrone, comprenons d'abord comment le code synchrone s'exécute à l'intérieur du moteur JavaScript. Par exemple:
const second = () => { console.log('Hello there!'); } const first = () => { console.log('Hi there!'); second(); console.log('The End'); } first();
Afin de comprendre comment le code ci-dessus est exécuté à l'intérieur du moteur JavaScript, nous devons comprendre le concept du contexte d'exécution et de la pile d'appels (également appelée pile d'exécution).
Contexte d'exécution
Le contexte d'exécution est un concept abstrait de l'environnement dans lequel le code est évalué et exécuté. Chaque fois qu'un code est exécuté en JavaScript, il s'exécute dans le contexte de l'exécution.
Le code de fonction est exécuté dans le contexte de l'exécution de la fonction, et le code global, à son tour, est exécuté dans le contexte d'exécution global. Chaque fonction a son propre contexte d'exécution.
Pile d'appels
Une pile d'appels est une pile avec une structure LIFO (Last in, First Out, first used), qui est utilisée pour stocker tous les contextes d'exécution créés pendant l'exécution du code.
JavaScript n'a qu'une seule pile d'appels, car il s'agit d'un langage de programmation à thread unique. La structure LIFO signifie que les éléments ne peuvent être ajoutés et supprimés que par le haut de la pile.
Revenons maintenant à l'extrait de code ci-dessus et essayons de comprendre comment le moteur JavaScript l'exécute.
const second = () => { console.log('Hello there!'); } const first = () => { console.log('Hi there!'); second(); console.log('The End'); } first();

Alors qu'est-ce qui s'est passé ici?
Lorsque le code a commencé à s'exécuter, un contexte d'exécution global a été créé (représenté comme
main () ) et ajouté en haut de la pile d'appels. Lorsque l'appel à la fonction
first () est rencontré, il est également ajouté en haut de la pile.
Ensuite,
console.log ('Hi there!') Est placé en haut de la pile des appels, après exécution, il est supprimé de la pile. Après cela, nous appelons la fonction
second () , elle est donc placée en haut de la pile.
console.log ('Hello there!') est ajouté en haut de la pile et en est supprimé une fois l'exécution terminée. La
deuxième fonction
() est terminée, elle est également supprimée de la pile.
console.log ('The End') a été ajouté en haut de la pile et supprimé à la fin. Après cela, la
première fonction
() se termine et est également supprimée de la pile.
L'exécution du programme se termine, donc le contexte d'appel global (
main () ) est supprimé de la pile.
Comment fonctionne le JavaScript asynchrone?
Maintenant que nous avons une compréhension de base de la pile d'appels et du fonctionnement de JavaScript synchrone, revenons au JavaScript asynchrone.
Qu'est-ce que le blocage?
Supposons que nous traitons le traitement d'image ou la demande de réseau de manière synchrone. Par exemple:
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();
Le traitement d'image et la demande de réseau prennent du temps. Lorsque la fonction
processImage () est appelée, son exécution prendra un certain temps, selon la taille de l'image.
Lorsque la fonction
processImage () est terminée, elle est supprimée de la pile. Après cela, la fonction
networkRequest () est appelée et ajoutée à la pile. Cela prendra encore du temps avant de terminer l'exécution.
En fin de compte, lorsque la fonction
networkRequest () est exécutée, la fonction
salutation () est appelée, car elle ne contient que la méthode
console.log , et cette méthode s'exécute généralement rapidement, la fonction
salutation () s'exécutera et se terminera instantanément.
Comme vous pouvez le voir, nous devons attendre que la fonction (comme
processImage () ou
networkRequest () ) se termine. Cela signifie que ces fonctions bloquent la pile d'appels ou le thread principal. Par conséquent, nous ne pouvons pas effectuer d'autres opérations tant que le code ci-dessus n'est pas exécuté.
Quelle est donc la solution?
La solution la plus simple est les fonctions de rappel asynchrones. Nous les utilisons pour rendre notre code non bloquant. Par exemple:
const networkRequest = () => { setTimeout(() => { console.log('Async Code'); }, 2000); }; console.log('Hello World'); networkRequest();
Ici, j'ai utilisé la méthode
setTimeout pour simuler une demande réseau. N'oubliez pas que
setTimeout ne
fait pas partie du moteur JavaScript, il fait partie des soi-disant API Web (dans le navigateur) et des API C / C ++ (dans node.js).
Afin de comprendre comment ce code est exécuté, nous devons traiter avec quelques concepts supplémentaires, tels que la boucle d'événements et la file d'attente de rappel (également appelée file d'attente de tâches ou file d'attente de messages).

La boucle d'événements, l'API Web et la file d'attente de messages / file d'attente de tâches ne font pas partie du moteur JavaScript; elles font partie de l'exécution JavaScript JavaScript ou de l'exécution JavaScript dans Nodejs (dans le cas de Nodejs). Dans Nodejs, les API Web sont remplacées par des API C / C ++.
Maintenant, revenons au code ci-dessus et voyons ce qui se passe dans le cas d'une exécution asynchrone.
const networkRequest = () => { setTimeout(() => { console.log('Async Code'); }, 2000); }; console.log('Hello World'); networkRequest(); console.log('The End');

Lorsque le code ci-dessus est chargé dans le navigateur,
console.log ('Hello World') est ajouté à la pile et supprimé de celle-ci à la fin de l'exécution. Ensuite, un appel à la fonction
networkRequest () est
rencontré ; il est ajouté en haut de la pile.
Ensuite, la fonction
setTimeout () est appelée et placée en haut de la pile. La fonction
setTimeout () a 2 arguments: 1) une fonction de rappel et 2) le temps en millisecondes.
setTimeout () démarre un minuteur pendant 2 secondes dans un environnement API Web. À ce stade,
setTimeout () se termine et est supprimé de la pile. Après cela,
console.log ('The End') est ajouté à la pile, exécuté et supprimé de celle-ci à la fin.
Pendant ce temps, le temporisateur a expiré, maintenant le rappel est ajouté à la file d'attente des messages. Mais le rappel ne peut pas être exécuté immédiatement, et c'est ici que le cycle de traitement des événements entre dans le processus.
Boucle d'événement
La tâche de la boucle d'événements est de garder une trace de la pile d'appels et de déterminer si elle est vide ou non. Si la pile d'appels est vide, la boucle d'événements recherche dans la file d'attente des messages pour voir s'il y a des rappels en attente d'être terminés.
Dans notre cas, la file d'attente de messages contient un rappel et la pile d'exécution est vide. Par conséquent, la boucle d'événements ajoute un rappel en haut de la pile.
Après que
console.log ('Code Async') est ajouté en haut de la pile, exécuté et supprimé de celle-ci. À ce stade, le rappel est terminé et supprimé de la pile et le programme est complètement terminé.
Événements DOM
La file d'attente de messages contient également des rappels d'événements DOM, tels que des clics et des événements de clavier. Par exemple:
document.querySelector('.btn').addEventListener('click',(event) => { console.log('Button Clicked'); });
Dans le cas d'événements DOM, le gestionnaire d'événements est entouré par l'API Web, attendant un événement spécifique (dans ce cas, un clic), et lorsque cet événement se produit, la fonction de rappel est placée dans la file d'attente de messages, en attendant son exécution.
Nous avons appris comment les rappels asynchrones et les événements DOM sont exécutés, qui utilisent une file d'attente de messages pour stocker les rappels en attente d'exécution.
ES6 MicroTask Queue
Remarque auteur de la traduction: Dans l'article, l'auteur a utilisé la file d'attente de messages / tâches et la file d'attente de travaux / micro-prises, mais si vous traduisez la file d'attente de tâches et la file d'attente de travaux, alors en théorie, cela se révèle la même chose. J'ai parlé avec l'auteur de la traduction et j'ai décidé d'omettre simplement le concept de file d'attente. Si vous avez des idées à ce sujet, je vous attends dans les commentaires
Lien vers la traduction de l'article par des promesses du même auteur
ES6 a introduit le concept de file d'attente de microtâches, qui est utilisé par Promises en JavaScript. La différence entre la file d'attente de messages et la file d'attente de microtâches est que la file d'attente de microtâches a une priorité plus élevée que la file d'attente de messages, ce qui signifie que les «promesses» à l'intérieur de la file d'attente de microtâches seront exécutées plus tôt que les rappels dans la file d'attente de messages.
Par exemple:
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');
Conclusion:
Script start Script End Promise resolved setTimeout
Comme vous pouvez le voir, la «promesse» a été exécutée avant
setTimeout , tout cela parce que la réponse de la «promesse» est stockée dans la file d'attente de microtâches, qui a une priorité plus élevée que la file d'attente de messages.
Regardons l'exemple suivant, cette fois 2 «promesses» et 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');
Conclusion:
Script start Script End Promise 1 resolved Promise 2 resolved setTimeout 1 setTimeout 2
Et encore une fois, nos deux «promesses» ont été exécutées avant les rappels à l'intérieur de
setTimeout , car la boucle de traitement des événements considère les tâches de la file d'attente de microtâches plus importantes que les tâches de la file d'attente de messages / file d'attente de tâches.
Si, lors de l'exécution des tâches, une autre "promesse" apparaît dans la file d'attente des microtâches, elle sera ajoutée à la fin de cette file d'attente et exécutée avant les rappels de la file d'attente des messages, et peu importe le temps d'attente pour leur exécution.
Par exemple:
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');
Conclusion:
Script start Script End Promise 1 resolved Promise 2 resolved Promise 3 resolved setTimeout
Ainsi, toutes les tâches de la file d'attente de microtâches seront terminées avant les tâches de la file d'attente de messages. C'est-à-dire que la boucle de traitement des événements effacera d'abord la file d'attente des microtâches, et alors seulement, elle commencera à exécuter des rappels à partir de la file d'attente de messages.
Conclusion
Nous avons donc appris le fonctionnement et les concepts de JavaScript asynchrone: pile d'appels, boucle d'événements, file d'attente de messages / file d'attente de tâches et file d'attente de microtâches qui constituent le runtime JavaScript