Ces deux réponses les mieux notées sont fausses. Consultez la description de MDN sur le modèle de concurrence et la boucle d'événements , et il devrait devenir clair ce qui se passe (que la ressource MDN est un véritable joyau). Et simplement utiliser setTimeout
peut être ajouter des problèmes inattendus dans votre code en plus de "résoudre" ce petit problème.
Ce qui se passe réellement ici n'est pas que "le navigateur n'est peut-être pas encore tout à fait prêt à cause de la concurrence", ou quelque chose basé sur "chaque ligne est un événement qui est ajouté à l'arrière de la file d'attente".
Le jsfiddle fourni par DVK illustre en effet un problème, mais son explication n'est pas correcte.
Ce qui se passe dans son code, c'est qu'il attache d'abord un gestionnaire d'événements à l' click
événement sur le #do
bouton.
Ensuite, lorsque vous cliquez réellement sur le bouton, un message
est créé faisant référence à la fonction de gestionnaire d'événements, qui est ajoutée au message queue
. Lorsque le event loop
atteint ce message, il crée un frame
sur la pile, avec l'appel de fonction au gestionnaire d'événements click dans le jsfiddle.
Et c'est là que ça devient intéressant. Nous sommes tellement habitués à penser que Javascript est asynchrone que nous sommes enclins à ignorer ce petit fait: toute image doit être exécutée, en totalité, avant que l'image suivante puisse être exécutée . Pas de simultanéité, les gens.
Qu'est-ce que ça veut dire? Cela signifie que chaque fois qu'une fonction est invoquée à partir de la file d'attente de messages, elle bloque la file d'attente jusqu'à ce que la pile qu'elle génère ait été vidée. Ou, en termes plus généraux, il bloque jusqu'à ce que la fonction soit revenue. Et il bloque tout , y compris les opérations de rendu DOM, le défilement et ainsi de suite. Si vous voulez une confirmation, essayez simplement d'augmenter la durée de la longue opération dans le violon (par exemple, exécutez la boucle externe 10 fois de plus), et vous remarquerez que pendant qu'elle s'exécute, vous ne pouvez pas faire défiler la page. S'il s'exécute suffisamment longtemps, votre navigateur vous demandera si vous souhaitez interrompre le processus, car cela rend la page insensible. La trame est en cours d'exécution et la boucle d'événements et la file d'attente de messages sont bloquées jusqu'à la fin.
Alors pourquoi cet effet secondaire du texte ne se met-il pas à jour? Parce que pendant que vous avez changé la valeur de l'élément dans le DOM - vous pouvez console.log()
sa valeur immédiatement après l'avoir changé et voir qu'il a été changé (ce qui montre pourquoi l'explication de DVK n'est pas correcte) - le navigateur attend que la pile s'épuise (la on
fonction de gestionnaire à renvoyer) et donc le message à terminer, afin qu'il puisse éventuellement exécuter le message qui a été ajouté par le runtime en réaction à notre opération de mutation, et afin de refléter cette mutation dans l'interface utilisateur .
C'est parce que nous attendons réellement que le code se termine. Nous n'avons pas dit "quelqu'un va chercher ceci et ensuite appeler cette fonction avec les résultats, merci, et maintenant j'ai terminé donc imma return, faites tout maintenant", comme nous le faisons habituellement avec notre Javascript asynchrone basé sur les événements. Nous entrons dans une fonction de gestionnaire d'événements de clic, nous mettons à jour un élément DOM, nous appelons une autre fonction, l'autre fonction fonctionne pendant longtemps puis revient, nous mettons ensuite à jour le même élément DOM, puis nous revenons de la fonction initiale, en vidant efficacement la pile. Et puis le navigateur peut accéder au message suivant dans la file d'attente, qui pourrait très bien être un message généré par nous en déclenchant un événement interne de type "on-DOM-mutation".
L'interface utilisateur du navigateur ne peut pas (ou choisit de ne pas) mettre à jour l'interface utilisateur jusqu'à ce que la trame en cours d'exécution soit terminée (la fonction est revenue). Personnellement, je pense que c'est plutôt par conception que par restriction.
Pourquoi ça setTimeout
marche alors? Il le fait, car il supprime efficacement l'appel à la fonction de longue durée de sa propre trame, en le planifiant pour être exécuté plus tard dans le window
contexte, afin qu'il puisse lui-même revenir immédiatement et permettre à la file d'attente de messages de traiter d'autres messages. Et l'idée est que le message d'interface utilisateur "mis à jour" qui a été déclenché par nous en Javascript lors de la modification du texte dans le DOM est désormais en avance sur le message mis en file d'attente pour la fonction de longue durée, de sorte que la mise à jour de l'interface utilisateur se produit avant le blocage pendant longtemps.
Notez que a) La fonction longue durée bloque toujours tout lors de son exécution, et b) vous n'êtes pas assuré que la mise à jour de l'interface utilisateur est en avance sur elle dans la file d'attente de messages. Sur mon navigateur Chrome de juin 2018, une valeur de 0
ne "résout" pas le problème que le violon démontre - 10 le fait. Je suis en fait un peu étouffé par cela, car il me semble logique que le message de mise à jour de l'interface utilisateur doive être mis en file d'attente avant, car son déclencheur est exécuté avant de planifier la fonction de longue durée à exécuter "plus tard". Mais peut-être qu'il y a des optimisations dans le moteur V8 qui peuvent interférer, ou peut-être que ma compréhension fait juste défaut.
D'accord, alors quel est le problème avec l'utilisation setTimeout
, et quelle est la meilleure solution pour ce cas particulier?
Tout d'abord, le problème avec l'utilisation setTimeout
de n'importe quel gestionnaire d'événements comme celui-ci, pour essayer de résoudre un autre problème, est susceptible de jouer avec un autre code. Voici un exemple réel de mon travail:
Un collègue, mal informé sur la boucle d'événement, a essayé de "threader" Javascript en utilisant du code de rendu de modèle setTimeout 0
pour son rendu. Il n'est plus ici pour demander, mais je peux supposer qu'il a peut-être inséré des minuteries pour mesurer la vitesse de rendu (qui serait l'immédiateté de retour des fonctions) et a constaté que l'utilisation de cette approche entraînerait des réponses extrêmement rapides de cette fonction.
Le premier problème est évident; vous ne pouvez pas enfiler javascript, vous ne gagnez donc rien ici pendant que vous ajoutez l'obscurcissement. Deuxièmement, vous avez maintenant détaché efficacement le rendu d'un modèle de la pile des écouteurs d'événements possibles qui pourraient s'attendre à ce que ce modèle ait été rendu, alors qu'il pourrait fort bien ne pas l'être. Le comportement réel de cette fonction était désormais non déterministe, tout comme - à son insu - toute fonction qui l'exécuterait ou en dépendrait. Vous pouvez faire des suppositions éclairées, mais vous ne pouvez pas coder correctement son comportement.
Le "correctif" lors de l'écriture d'un nouveau gestionnaire d'événements qui dépendait de sa logique devait également être utilisé setTimeout 0
. Mais, ce n'est pas un correctif, c'est difficile à comprendre et ce n'est pas amusant de déboguer les erreurs causées par un code comme celui-ci. Parfois, il n'y a jamais de problème, d'autres fois, il échoue de manière concise, puis, parfois, cela fonctionne et se casse sporadiquement, en fonction des performances actuelles de la plate-forme et de tout ce qui se passe à ce moment-là. Voilà pourquoi je personnellement déconseiller l'utilisation du ce hack (il est un hack, et nous devrions tous savoir qu'il est), à moins que vous savez vraiment ce que vous faites et quelles sont les conséquences.
Mais que pouvons- nous faire à la place? Eh bien, comme le suggère l'article MDN référencé, divisez le travail en plusieurs messages (si vous le pouvez) afin que les autres messages mis en file d'attente soient entrelacés avec votre travail et exécutés pendant son exécution, ou utilisez un travailleur Web, qui peut s'exécuter en tandem avec votre page et renvoyez les résultats lorsque vous avez terminé ses calculs.
Oh, et si vous pensez: "Eh bien, ne pourrais-je pas simplement mettre un rappel dans la fonction longue durée pour la rendre asynchrone?", Alors non. Le rappel ne le rend pas asynchrone, il devra toujours exécuter le code de longue durée avant d'appeler explicitement votre rappel.