J'essayais un exemple d'un livre pour confirmer le fonctionnement de la boucle d'événements JavaScript, voici le code
foo baz baz bar
Le fonctionnement de setTimeout ici (exécuté dans le désordre) est simple.
const baz = () => console.log("baz"); const bar = () => console.log("bar"); const foo = () => { console.log("foo"); setTimeout(bar, 0); baz(); } setTimeout(baz, 0); // this somehow runs before foo() is finished foo();
Ce que je ne comprends pas, c'est la commande lorsque j'ai ajouté une ligne
foo baz bar
La sortie est
const baz = () => console.log("baz"); const bar = () => console.log("bar"); const foo = () => { console.log("foo"); setTimeout(bar, 0); baz(); } foo();
Comment se fait-il que le deuxième setTimeout se rince avant que foo () ne soit terminé?
4 Réponses :
Vous pouvez le voir clairement si vous séparez les opérations asynchrones du reste du code:
baz bar
Si nous extrayons les fonctions setTimeout
, nous aurons:
setTimeout(baz, 0); setTimeout(bar, 0);
qui enregistre les deux premières lignes:
foo baz
Lors de la première boucle, les fonctions asynchrones définies dans setTimeout
ont été ajoutées à la boucle suivante, nous devons donc les exécuter:
const baz = () => console.log("baz"); const bar = () => console.log("bar"); const foo = () => { console.log("foo"); // setTimeout(bar, 0); baz(); } // setTimeout(baz, 0); // this somehow runs before foo() is finished foo();
Résultat dans les deux prochaines lignes du journal:
const baz = () => console.log("baz"); const bar = () => console.log("bar"); const foo = () => { console.log("foo"); setTimeout(bar, 0); baz(); } setTimeout(baz, 0); // this somehow runs before foo() is finished foo();
Vous devez garder à l'esprit que tout le code synchrone s'exécutera en premier, puis le code asynchrone. ( setTimeout
mettra toujours en file d'attente les actions asynchrones, même si elles sont définies avec un timeout de 0.)
Donc, dans cet esprit, l'ordre des événements est le suivant:
baz()
foo
bar()
baz()
, sortie de baz
Sachant que la synchronisation fonctionne en premier, nous obtenons d'abord foo
puis baz
.
Ensuite, nos événements asynchrones s'exécutent, produisant tour à tour baz
et bar
.
Vous appelez la fonction setTimeout(baz, 0)
, elle va à la pile d'appels, puis à la phase des minuteries dans la boucle d'événement, et attendez là.
Après avoir appelé la fonction foo()
, qui insère dans la pile d'appels console.log("foo")
, setTimeout(bar, 0)
et baz()
. Comme console.log("foo")
est une opération synchrone, elle est exécutée immédiatement et vous voyez "foo"
dans la sortie. setTimeout(bar, 0)
passe à la phase des minuteries dans la boucle d'événements et attend. Ensuite, exécutez la fonction baz()
, qui à son tour lance console.log("baz")
, et vous voyez le "baz"
dans la sortie. Après avoir exécuté les opérations synchrones, la pile d'appels est vide et l'attente des minuteries est terminée, commençant à exécuter les rappels à partir de setTimeouts
.
setTimeout(baz, 0)
-> baz
-> console.log("baz")
= "baz"
dans l'otput
et après ça
setTimeout(bar, 0)
-> bar
-> console.log("bar")
= "bar"
dans l'otput
Voici une explication du point de vue de la boucle d'événements.
Vous pouvez visualiser la pile d'appels, qui est utilisée pour garder une trace de l'endroit où nous nous trouvons dans un programme à un moment donné. Lorsque vous appelez une fonction, nous la poussons sur la pile, et lorsque nous retournons / terminons une fonction, nous la sortons du haut de la pile. Au départ, la pile est vide.
Lorsque vous exécutez votre code pour la première fois, votre "script" principal sera poussé sur la pile, et sera sorti de la pile une fois que le script aura fini de s'exécuter:
"foo" "baz" "baz" "bar"
nous définissons ensuite quelques fonctions baz
, bar
et foo
, et foo
par atteindre notre première invocation de fonction, setTimeout(baz, 0)
, et ainsi, nous la poussons sur la pile:
Stack: ------ <EMPTY> Task Queue: (Front <--- Back) bar
setTimeout()
lance une API Web qui, après 0
ms, met en file d'attente votre rappel baz
dans la file d'attente des tâches . Une fois que setTimeout
a transmis son travail à l'API Web, son travail est terminé et il a donc terminé son travail et peut être retiré de la pile:
Stack: ------ <EMPTY> Task Queue: (Front <--- Back) baz, bar
C'est le travail de la boucle d'événements de prendre des tâches de la file d'attente de tâches et de les pousser sur la pile lorsque la pile est vide . Actuellement, la pile n'est pas vide car nous sommes toujours dans le script principal, donc baz()
ne s'est pas encore exécuté. La prochaine invocation de fonction que nous rencontrons est foo()
, donc nous poussons ceci sur notre pile:
Stack: ------ - foo() - Main() Task Queue: (Front <--- Back) baz, bar
Foo appelle ensuite la méthode log()
l'objet console, qui est également poussée sur la pile:
Stack: ------ - log("foo") - foo() - Main() Task Queue: (Front <--- Back) baz
Cela enregistre "foo"
et log()
est sorti de la pile une fois qu'il a terminé son travail. Nous continuons ensuite à parcourir la fonction foo. Nous rencontrons maintenant un appel de fonction à setTimeout(bar, 0);
. Ceci, tout comme le premier appel de fonction, pousse setTimeout(bar, 0)
sur la pile. Cela fait tourner une API Web qui ajoute une bar
à la file d'attente des tâches. setTimeout(bar, 0)
est également terminé une fois qu'il a transmis son travail à l'api Web, il est donc également retiré de la pile (voir les deuxième et troisième schémas ascii pour ces étapes), nous laissant avec:
Stack: ------ - foo() - Main() Task Queue: (Front <--- Back) baz
Enfin, nous arrivons à la dernière ligne de la fonction foo
qui appelle baz()
. Cela pousse baz()
sur la pile d'appels, puis pousse le log("baz")
vers le haut de la pile d'appels, qui enregistre "baz" . Jusqu'à présent, nous avons enregistré "foo" puis "baz". Une fois que baz a été consigné, log()
est sorti de la pile, tout comme baz()
terminé.
Une fois que la dernière ligne de foo()
est terminée, nous retournons implicitement, sortant foo()
de la pile, nous laissant avec Main()
. Une fois que nous sommes revenus de foo, notre contrôle / exécution est retourné au script principal après où foo()
été invoqué. Comme il n'y a plus de fonctions à appeler dans notre script, nous sortons Main()
de la pile, nous laissant avec:
Stack: ------ - Main() Task Queue: (Front <--- Back) baz
Maintenant que la pile est vide, la boucle d'événements peut entrer et gérer baz
et bar
dans la file d'attente des tâches. Tout d'abord, il sort baz
de la file d'attente et le pousse sur la pile, qui appelle ensuite log("baz")
, en poussant le log
sur la pile puis en enregistrant "baz" . Une fois le journal terminé, le log
et le baz
sont retirés de la pile en la laissant à nouveau vide:
Stack: ------ - setTimeout(baz, 0) - Main()
Maintenant que la pile est à nouveau vide, la boucle d'événement prend la première tâche de la file d'attente (c'est-à-dire: bar
) et la pousse dans la pile. bar
appelle alors log("bar")
, qui ajoute le log("bar")
à la pile, ainsi que les logs "bar" à la console. Une fois la journalisation terminée, log()
et bar()
sont tous deux sortis de la pile.
En conséquence, la sortie de vos journaux est imprimée dans l'ordre suivant (voir les journaux en gras ci-dessus):
Stack: ------ - Main() // <-- indicates that we're in the main script
Quelques bonnes ressources sur la boucle d'événements et la pile d'appels peuvent être trouvées ici , ici et ici .
Considérez la boucle d'événements comme une file d'attente, votre code insère quelque chose à faire plus tard avant d'appeler foo, appelez foo, foo ajoute quelque chose d'autre à la file d'attente, foo se termine, la boucle d'événements extrait de la file d'attente la première soumission
Ah, je vois que je comprends maintenant beaucoup de mercis