J'ai récemment rencontré un bug plutôt méchant, dans lequel le code chargeait <select>
dynamiquement un via JavaScript. Ce chargement dynamique <select>
avait une valeur présélectionnée. Dans IE6, nous avions déjà le code pour corriger sélectionné <option>
, parce que parfois la <select>
« s selectedIndex
valeur serait synchronisé avec sélectionné <option>
l » index
attribut, comme ci - dessous:
field.selectedIndex = element.index;
Cependant, ce code ne fonctionnait pas. Même si le champ selectedIndex
était correctement défini, le mauvais index finirait par être sélectionné. Cependant, si je collais une alert()
déclaration au bon moment, l'option correcte serait sélectionnée. Pensant que cela pourrait être une sorte de problème de synchronisation, j'ai essayé quelque chose de aléatoire que j'avais vu auparavant dans le code:
var wrapFn = (function() {
var myField = field;
var myElement = element;
return function() {
myField.selectedIndex = myElement.index;
}
})();
setTimeout(wrapFn, 0);
Et cela a fonctionné!
J'ai une solution à mon problème, mais je suis inquiet de ne pas savoir exactement pourquoi cela résout mon problème. Quelqu'un at-il une explication officielle? Quel problème de navigateur est-ce que j'évite en appelant ma fonction "plus tard" en utilisant setTimeout()
?
la source
setTimeout(fn)
est le même quesetTimeout(fn, 0)
, btw.Réponses:
Dans la question, il existait une condition de concurrence entre:
Votre code gagnait régulièrement cette course et tentait de définir une sélection déroulante avant que le navigateur ne soit prêt, ce qui signifie que le bogue apparaîtrait.
Cette course existait parce que JavaScript a un seul thread d'exécution qui est partagé avec le rendu de page. En effet, l'exécution de JavaScript bloque la mise à jour du DOM.
Votre solution de contournement était:
L'appel
setTimeout
avec un rappel et zéro comme deuxième argument planifiera le rappel pour qu'il s'exécute de manière asynchrone , après le délai le plus court possible - qui sera d'environ 10 ms lorsque l'onglet a le focus et que le thread d'exécution JavaScript n'est pas occupé.La solution de l'OP était donc de retarder d'environ 10 ms le réglage de l'indice sélectionné. Cela a donné au navigateur l'occasion d'initialiser le DOM, corrigeant le bogue.
Chaque version d'Internet Explorer présentait des comportements originaux et ce type de solution était parfois nécessaire. Alternativement, il pourrait s'agir d'un véritable bug dans la base de code de l'OP.
Voir la conférence de Philip Roberts "Qu'est-ce que c'est que la boucle d'événement?" pour une explication plus approfondie.
la source
Préface:
Certaines des autres réponses sont correctes mais n'illustrent pas réellement le problème à résoudre, j'ai donc créé cette réponse pour présenter cette illustration détaillée.
À ce titre, je publie une présentation détaillée de ce que fait le navigateur et de la façon dont l'aide
setTimeout()
aide . Il a l'air assez long mais est en fait très simple et direct - je viens de le rendre très détaillé.MISE À JOUR: J'ai fait un JSFiddle pour démontrer en direct l'explication ci-dessous: http://jsfiddle.net/C2YBE/31/ . Un grand merci à @ThangChung pour avoir aidé à le démarrer.
MISE À JOUR2: Juste au cas où le site Web JSFiddle mourrait ou supprimerait le code, j'ai ajouté le code à cette réponse à la toute fin.
DÉTAILS :
Imaginez une application web avec un bouton "faire quelque chose" et un résultat div.
Le
onClick
gestionnaire du bouton "faire quelque chose" appelle une fonction "LongCalc ()", qui fait 2 choses:Effectue un calcul très long (disons 3 minutes)
Imprime les résultats du calcul dans le résultat div.
Maintenant, vos utilisateurs commencent à tester cela, cliquez sur le bouton "faire quelque chose", et la page se trouve là sans rien faire pendant 3 minutes, ils deviennent agités, cliquez à nouveau sur le bouton, attendez 1 minute, rien ne se passe, cliquez à nouveau sur le bouton ...
Le problème est évident - vous voulez un DIV "Status", qui montre ce qui se passe. Voyons comment cela fonctionne.
Vous ajoutez donc un DIV "Status" (initialement vide) et modifiez le
onclick
gestionnaire (fonctionLongCalc()
) pour faire 4 choses:Remplissez le statut "Le calcul ... peut prendre environ 3 minutes" dans le statut DIV
Effectue un calcul très long (disons 3 minutes)
Imprime les résultats du calcul dans le résultat div.
Remplissez le statut "Calcul effectué" dans le statut DIV
Et, vous donnez volontiers l'application aux utilisateurs pour la tester à nouveau.
Ils vous reviennent très en colère. Et expliquez que lorsqu'ils ont cliqué sur le bouton, le statut DIV n'a jamais été mis à jour avec le statut "Calcul ..." !!!
Vous vous grattez la tête, demandez autour de StackOverflow (ou lisez des documents ou Google) et réalisez le problème:
Le navigateur place toutes ses tâches "TODO" (tâches d'interface utilisateur et commandes JavaScript) résultant d'événements dans une seule file d'attente . Et malheureusement, redessiner le DIV "Statut" avec la nouvelle valeur "Calcul ..." est un TODO distinct qui va à la fin de la file d'attente!
Voici une ventilation des événements pendant le test de votre utilisateur, le contenu de la file d'attente après chaque événement:
[Empty]
[Execute OnClick handler(lines 1-4)]
[Execute OnClick handler(lines 2-4), re-draw Status DIV with new "Calculating" value]
. Veuillez noter que bien que les changements DOM se produisent instantanément, pour redessiner l'élément DOM correspondant, vous avez besoin d'un nouvel événement, déclenché par le changement DOM, qui s'est produit à la fin de la file d'attente .[Execute OnClick handler(lines 3-4), re-draw Status DIV with "Calculating" value]
.[Execute OnClick handler(line 4), re-draw Status DIV with "Calculating" value, re-draw result DIV with result]
.[Execute OnClick handler, re-draw Status DIV with "Calculating" value, re-draw result DIV with result; re-draw Status DIV with "DONE" value]
.return
duonclick
gestionnaire sub. Nous retirons le «gestionnaire d'exécution OnClick» de la file d'attente et commençons à exécuter l'élément suivant de la file d'attente.Ainsi, le problème sous-jacent est que l'événement de redessiner pour DIV "Status" est placé dans la file d'attente à la fin, APRÈS l'événement "exécuter la ligne 2" qui prend 3 minutes, donc le redessin réel ne se produit pas avant APRÈS le calcul est fait.
À la rescousse vient le
setTimeout()
. Comment ça aide? Parce qu'en appelant du code à exécution longue viasetTimeout
, vous créez en fait 2 événements: l'setTimeout
exécution elle-même et (en raison du délai d'expiration 0), une entrée de file d'attente distincte pour le code en cours d'exécution.Donc, pour résoudre votre problème, vous modifiez votre
onClick
gestionnaire pour qu'il soit DEUX instructions (dans une nouvelle fonction ou simplement un bloc à l'intérieuronClick
):Remplissez le statut "Le calcul ... peut prendre environ 3 minutes" dans le statut DIV
Exécuter
setTimeout()
avec 0 timeout et un appel à laLongCalc()
fonction .LongCalc()
la fonction est presque la même que la dernière fois mais n'a évidemment pas la mise à jour du statut "Calcul ..." comme première étape; et commence à la place le calcul immédiatement.Alors, à quoi ressemble la séquence d'événements et la file d'attente maintenant?
[Empty]
[Execute OnClick handler(status update, setTimeout() call)]
[Execute OnClick handler(which is a setTimeout call), re-draw Status DIV with new "Calculating" value]
.[re-draw Status DIV with "Calculating" value]
. La file d'attente n'a rien de nouveau pendant 0 seconde de plus.[re-draw Status DIV with "Calculating" value, execute LongCalc (lines 1-3)]
.[execute LongCalc (lines 1-3)]
. Veuillez noter que cet événement de nouveau tirage peut effectivement se produire AVANT que l'alarme ne se déclenche, ce qui fonctionne tout aussi bien.Hourra! Le Status DIV vient d'être mis à jour en "Calcul ..." avant le début du calcul !!!
Ci-dessous, l'exemple de code du JSFiddle illustrant ces exemples: http://jsfiddle.net/C2YBE/31/ :
Code HTML:
Code JavaScript: (exécuté sur
onDomReady
et peut nécessiter jQuery 1.9)la source
Jetez un œil à l'article de John Resig sur le fonctionnement des minuteurs JavaScript . Lorsque vous définissez un délai d'expiration, il met en file d'attente le code asynchrone jusqu'à ce que le moteur exécute la pile d'appels actuelle.
la source
setTimeout()
vous fait gagner du temps jusqu'à ce que les éléments DOM soient chargés, même s'il est défini sur 0.Vérifiez ceci: setTimeout
la source
La plupart des navigateurs ont un processus appelé thread principal, qui est chargé d'exécuter certaines tâches JavaScript, les mises à jour de l'interface utilisateur, par exemple: la peinture, le redessinage ou la refusion, etc.
Certaines tâches d'exécution JavaScript et de mise à jour de l'interface utilisateur sont mises en file d'attente dans la file d'attente des messages du navigateur, puis sont envoyées au thread principal du navigateur pour être exécutées.
Lorsque des mises à jour de l'interface utilisateur sont générées alors que le thread principal est occupé, les tâches sont ajoutées dans la file d'attente de messages.
setTimeout(fn, 0);
ajoutez-lefn
à la fin de la file d'attente à exécuter. Il planifie une tâche à ajouter dans la file d'attente de messages après un certain temps.la source
add this fn to the end of the queue
. Le plus important est où exactementsetTimeout
ajoute cette fonction, fin de ce cycle de boucle ou tout début du cycle de boucle suivant.Il y a ici des réponses contradictoires et sans preuve, il n'y a aucun moyen de savoir à qui croire. Voici la preuve que @DVK a raison et @SalvadorDali est incorrect. Ce dernier affirme:
Le délai d'expiration minimum de 4 ms n'est pas pertinent pour ce qui se passe. Ce qui se passe vraiment, c'est que setTimeout pousse la fonction de rappel à la fin de la file d'attente d'exécution. Si après setTimeout (rappel, 0) vous avez un code de blocage qui prend plusieurs secondes à s'exécuter, le rappel ne sera pas exécuté pendant plusieurs secondes, jusqu'à ce que le code de blocage soit terminé. Essayez ce code:
La sortie est:
la source
Une des raisons pour cela est de reporter l'exécution du code à une boucle d'événements ultérieure distincte. Lorsque vous répondez à un événement de navigateur d'une sorte (clic de souris, par exemple), il est parfois nécessaire d'effectuer des opérations uniquement après le traitement de l'événement en cours. L'
setTimeout()
installation est le moyen le plus simple de le faire.éditez maintenant que nous sommes en 2015, je dois noter qu'il y en a aussi
requestAnimationFrame()
, ce qui n'est pas exactement le même mais il est suffisamment proche desetTimeout(fn, 0)
celui qui mérite d'être mentionné.la source
Il s'agit d'une vieille question avec de vieilles réponses. Je voulais ajouter un nouveau regard sur ce problème et répondre pourquoi cela se produit et non pourquoi est-ce utile.
Vous avez donc deux fonctions:
puis appelez-les dans l'ordre suivant
f1(); f2();
pour voir que le second s'exécute en premier.Et voici pourquoi: il n'est pas possible d'avoir
setTimeout
avec un retard de 0 millisecondes. La valeur minimale est déterminée par le navigateur et elle n'est pas de 0 millisecondes. Historiquement, les navigateurs fixent ce minimum à 10 millisecondes, mais les spécifications HTML5 et les navigateurs modernes le fixent à 4 millisecondes.Aussi de mozilla:
Les informations PS sont prises après avoir lu l' article suivant .
la source
setTimeout(fn,0)
est utile.setTimeout
/setInterval
plus de cinq niveaux de profondeur; si vous ne l'avez pas fait, le minimum est de 0 ms (avec des machines à remonter le temps manquantes). Les documents de Mozilla développent cela pour couvrir les cas répétés, pas seulement imbriqués (doncsetInterval
avec un intervalle de 0, il sera immédiatement reprogrammé plusieurs fois, puis retardé plus tard), mais les utilisations simples desetTimeout
avec une imbrication minimale sont autorisées à faire immédiatement la queue.Puisqu'il est passé une durée de
0
, je suppose que c'est afin de supprimer le code passé ausetTimeout
flux d'exécution. Donc, si c'est une fonction qui peut prendre un certain temps, cela n'empêchera pas l'exécution du code suivant.la source
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 aumessage queue
. Lorsque leevent loop
atteint ce message, il crée unframe
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 (laon
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 lewindow
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.
la source
L'autre chose que cela fait est de pousser l'appel de fonction au bas de la pile, empêchant un débordement de pile si vous appelez récursivement une fonction. Cela a l'effet d'une
while
boucle mais permet au moteur JavaScript de déclencher d'autres temporisateurs asynchrones.la source
push the function invocation to the bottom of the stack
. Ce dontstack
vous parlez est obscur. Le plus important est où exactementsetTimeout
ajoute cette fonction, fin de ce cycle de boucle ou tout début du cycle de boucle suivant.En appelant setTimeout, vous donnez à la page le temps de réagir à ce que l'utilisateur fait. Ceci est particulièrement utile pour les fonctions exécutées pendant le chargement de la page.
la source
Quelques autres cas où setTimeout est utile:
Vous souhaitez rompre une boucle ou un calcul de longue durée en composants plus petits afin que le navigateur ne semble pas «geler» ou dire «Le script sur la page est occupé».
Vous souhaitez désactiver un bouton d'envoi de formulaire lorsque vous cliquez dessus, mais si vous désactivez le bouton dans le gestionnaire onClick, le formulaire ne sera pas soumis. setTimeout avec un temps de zéro fait l'affaire, permettant à l'événement de se terminer, au formulaire de commencer à se soumettre, puis votre bouton peut être désactivé.
la source
Les réponses concernant les boucles d'exécution et le rendu du DOM avant la fin de certains autres codes sont correctes. Les délais d'attente de zéro seconde dans JavaScript aident à rendre le code pseudo-multithread, même s'il ne l'est pas.
Je veux ajouter que la MEILLEURE valeur pour un délai d'attente de zéro seconde entre plusieurs navigateurs / plates-formes en JavaScript est en fait d'environ 20 millisecondes au lieu de 0 (zéro), car de nombreux navigateurs mobiles ne peuvent pas enregistrer des délais d'attente inférieurs à 20 millisecondes en raison des limitations d'horloge sur les puces AMD.
De plus, les processus de longue durée qui n'impliquent pas de manipulation DOM doivent être envoyés aux Web Workers maintenant, car ils fournissent une véritable exécution multithread de JavaScript.
la source
setTimout sur 0 est également très utile dans le schéma de configuration d'une promesse différée, que vous souhaitez retourner immédiatement:
la source
Le problème était que vous tentiez d'effectuer une opération Javascript sur un élément non existant. L'élément n'était pas encore chargé et
setTimeout()
donne plus de temps à un élément pour se charger des manières suivantes:setTimeout()
fait en sorte que l'événement soit ansynchrone, donc exécuté après tout le code synchrone, ce qui donne plus de temps à votre élément pour se charger. Les rappels asynchrones comme le rappelsetTimeout()
sont placés dans la file d'attente d'événements et placés sur la pile par la boucle d'événement fois que la pile de code synchrone est vide.setTimeout()
est souvent légèrement plus élevée (4-10 ms selon le navigateur). Ce temps légèrement plus long nécessaire à l'exécution dessetTimeout()
rappels est provoqué par la quantité de «ticks» (où un tick pousse un rappel sur la pile si la pile est vide) de la boucle d'événements. Pour des raisons de performances et de durée de vie de la batterie, le nombre de tics dans la boucle d'événements est limité à une certaine quantité inférieure à 1000 fois par seconde.la source
Javascript est une application à thread unique qui ne permet pas d'exécuter des fonctions simultanément. Pour cela, des boucles d'événement sont utilisées. Donc exactement ce que setTimeout (fn, 0) fait que sa quête de tâches est exécutée lorsque votre pile d'appels est vide. Je sais que cette explication est assez ennuyeuse, donc je vous recommande de parcourir cette vidéo, cela vous aidera à savoir comment les choses fonctionnent sous le capot dans le navigateur. Découvrez cette vidéo: - https://www.youtube.com/watch?time_continue=392&v=8aGhZQkoFbQ
la source