Pourquoi setTimeout (fn, 0) est-il parfois utile?

872

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 selectedIndexvaleur serait synchronisé avec sélectionné <option>l » indexattribut, 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()?

Dan Lew
la source
2
La plupart des questions décrivent pourquoi cela est utile. Si vous avez besoin de savoir pourquoi cela se produit - lisez ma réponse: stackoverflow.com/a/23747597/1090562
Salvador Dali
18
Philip Roberts explique cela de la meilleure façon possible ici dans son exposé "Qu'est-ce que c'est que la boucle d'événement?" youtube.com/watch?v=8aGhZQkoFbQ
vasa
Si vous êtes pressé, c'est la partie de la vidéo qu'il commence à répondre exactement à la question: youtu.be/8aGhZQkoFbQ?t=14m54s . Regarldless, toute la vidéo vaut la peine d'être regardée.
William Hampshire
4
setTimeout(fn)est le même que setTimeout(fn, 0), btw.
vsync

Réponses:

827

Dans la question, il existait une condition de concurrence entre:

  1. La tentative du navigateur d'initialiser la liste déroulante, prête à mettre à jour son index sélectionné, et
  2. Votre code pour définir l'index sélectionné

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:

setTimeout(callback, 0)

L'appel setTimeoutavec 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.

staticien
la source
276
«La solution consiste à« suspendre »l'exécution de JavaScript pour permettre aux threads de rendu de se rattraper.» Ce n'est pas tout à fait vrai, ce que setTimeout fait est d'ajouter un nouvel événement à la file d'attente d'événements du navigateur et le moteur de rendu est déjà dans cette file d'attente (pas tout à fait vrai, mais assez proche) afin qu'il soit exécuté avant l'événement setTimeout.
David Mulder
48
Oui, c'est une réponse beaucoup plus détaillée et beaucoup plus correcte. Mais le mien est "assez correct" pour que les gens comprennent pourquoi l'astuce fonctionne.
staticsan
2
@DavidMulder, cela signifie-t-il que le navigateur analyse le CSS et le rendu dans un thread différent du thread d'exécution javascript?
Jason
8
Non, ils sont analysés en principe dans le même thread, sinon quelques lignes de manipulation DOM déclencheraient des reflows tout le temps, ce qui aurait une très mauvaise influence sur la vitesse d'exécution.
David Mulder
30
Cette vidéo est la meilleure explication de la raison pour laquelle nous avons défini TimeTime
davibq
665

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 onClickgestionnaire du bouton "faire quelque chose" appelle une fonction "LongCalc ()", qui fait 2 choses:

  1. Effectue un calcul très long (disons 3 minutes)

  2. 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 onclickgestionnaire (fonction LongCalc()) pour faire 4 choses:

  1. Remplissez le statut "Le calcul ... peut prendre environ 3 minutes" dans le statut DIV

  2. Effectue un calcul très long (disons 3 minutes)

  3. Imprime les résultats du calcul dans le résultat div.

  4. 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:

  • Queue: [Empty]
  • Événement: cliquez sur le bouton. File d'attente après l'événement:[Execute OnClick handler(lines 1-4)]
  • Événement: exécution de la première ligne du gestionnaire OnClick (par exemple, modification de la valeur du statut DIV). File d' attente après l' événement: [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 .
  • PROBLÈME!!! PROBLÈME!!! Détails expliqués ci-dessous.
  • Événement: exécution de la deuxième ligne du gestionnaire (calcul). File d' attente après: [Execute OnClick handler(lines 3-4), re-draw Status DIV with "Calculating" value].
  • Événement: Exécuter la 3ème ligne dans le gestionnaire (remplir le résultat DIV). File d' attente après: [Execute OnClick handler(line 4), re-draw Status DIV with "Calculating" value, re-draw result DIV with result].
  • Événement: Exécutez la 4ème ligne dans le gestionnaire (remplissez le statut DIV avec "DONE"). Queue: [Execute OnClick handler, re-draw Status DIV with "Calculating" value, re-draw result DIV with result; re-draw Status DIV with "DONE" value].
  • Événement: exécution implicite returndu onclickgestionnaire 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.
  • REMARQUE: Puisque nous avons déjà terminé le calcul, 3 minutes se sont déjà écoulées pour l'utilisateur. L'événement de nouveau tirage n'a pas encore eu lieu !!!
  • Événement: redessiner le statut DIV avec la valeur "Calcul". Nous faisons le nouveau tirage et le retirons de la file d'attente.
  • Événement: redessinez le résultat DIV avec la valeur du résultat. Nous faisons le nouveau tirage et le retirons de la file d'attente.
  • Événement: redessiner le statut DIV avec la valeur "Terminé". Nous faisons le nouveau tirage et le retirons de la file d'attente. Les téléspectateurs aux yeux vifs pourraient même remarquer le clignotement de la valeur "Statut DIV avec" Calcul "pendant une fraction de microseconde - APRÈS LE CALCUL TERMINÉ

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 via setTimeout, vous créez en fait 2 événements: l' setTimeoutexé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 onClickgestionnaire pour qu'il soit DEUX instructions (dans une nouvelle fonction ou simplement un bloc à l'intérieur onClick):

  1. Remplissez le statut "Le calcul ... peut prendre environ 3 minutes" dans le statut DIV

  2. Exécuter setTimeout()avec 0 timeout et un appel à la LongCalc()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?

  • Queue: [Empty]
  • Événement: cliquez sur le bouton. File d'attente après l'événement:[Execute OnClick handler(status update, setTimeout() call)]
  • Événement: exécution de la première ligne dans le gestionnaire OnClick (par exemple, modification de la valeur du statut DIV). File d' attente après l' événement: [Execute OnClick handler(which is a setTimeout call), re-draw Status DIV with new "Calculating" value].
  • Événement: exécution de la deuxième ligne du gestionnaire (appel setTimeout). File d' attente après: [re-draw Status DIV with "Calculating" value]. La file d'attente n'a rien de nouveau pendant 0 seconde de plus.
  • Événement: l'alarme de la temporisation se déclenche, 0 seconde plus tard. File d' attente après: [re-draw Status DIV with "Calculating" value, execute LongCalc (lines 1-3)].
  • Événement: redessiner le statut DIV avec la valeur "Calcul" . File d' attente après: [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:

<table border=1>
    <tr><td><button id='do'>Do long calc - bad status!</button></td>
        <td><div id='status'>Not Calculating yet.</div></td>
    </tr>
    <tr><td><button id='do_ok'>Do long calc - good status!</button></td>
        <td><div id='status_ok'>Not Calculating yet.</div></td>
    </tr>
</table>

Code JavaScript: (exécuté sur onDomReadyet peut nécessiter jQuery 1.9)

function long_running(status_div) {

    var result = 0;
    // Use 1000/700/300 limits in Chrome, 
    //    300/100/100 in IE8, 
    //    1000/500/200 in FireFox
    // I have no idea why identical runtimes fail on diff browsers.
    for (var i = 0; i < 1000; i++) {
        for (var j = 0; j < 700; j++) {
            for (var k = 0; k < 300; k++) {
                result = result + i + j + k;
            }
        }
    }
    $(status_div).text('calculation done');
}

// Assign events to buttons
$('#do').on('click', function () {
    $('#status').text('calculating....');
    long_running('#status');
});

$('#do_ok').on('click', function () {
    $('#status_ok').text('calculating....');
    // This works on IE8. Works in Chrome
    // Does NOT work in FireFox 25 with timeout =0 or =1
    // DOES work in FF if you change timeout from 0 to 500
    window.setTimeout(function (){ long_running('#status_ok') }, 0);
});
DVK
la source
12
grande réponse DVK! Voici un résumé qui illustre votre exemple gist.github.com/kumikoda/5552511#file-timeout-html
kumikoda
4
Réponse vraiment cool, DVK. Juste pour rendre facile à imaginer, j'ai mis ce code à jsfiddle jsfiddle.net/thangchung/LVAaV
thangchung
4
@ThangChung - J'ai essayé de faire une meilleure version (2 boutons, un pour chaque cas) dans JSFiddle. Cela fonctionne comme une démo sur Chrome et IE mais pas sur FF pour une raison quelconque - voir jsfiddle.net/C2YBE/31 . J'ai demandé pourquoi FF ne fonctionne pas ici: stackoverflow.com/questions/20747591/…
DVK
1
@DVK "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". Monsieur, pouvez-vous s'il vous plaît fournir la source pour cela? Les navigateurs imho ne sont pas censés avoir une interface utilisateur (moteur de rendu) et des threads JS différents .... aucune infraction prévue .... je veux juste apprendre ..
bhavya_w
1
@bhavya_w Non, tout se passe sur un seul thread. C'est la raison pour laquelle un long calcul js peut bloquer l'interface utilisateur
Seba Kerckhof
88

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.

Andy
la source
23

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

Jose Basilio
la source
23

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-le fnà 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.

arley
la source
"Toutes les tâches d'exécution JavaScript et de mise à jour de l'interface utilisateur sont ajoutées au système de file d'attente d'événements du navigateur, puis ces tâches sont envoyées au thread d'interface utilisateur principal du navigateur à effectuer." .... source s'il vous plaît?
bhavya_w
4
JavaScript haute performance (Nicholas Zakas, Stoyan Stefanov, Ross Harmes, Julien Lecomte et Matt Sweeney)
Arley
Votez pour cela add this fn to the end of the queue. Le plus important est où exactement setTimeoutajoute cette fonction, fin de ce cycle de boucle ou tout début du cycle de boucle suivant.
Vert
19

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:

"Et voici pourquoi: il n'est pas possible d'avoir setTimeout avec un délai de 0 millisecondes. La valeur minimale est déterminée par le navigateur et ce n'est pas 0 millisecondes. Historiquement, les navigateurs définissent ce minimum à 10 millisecondes, mais les spécifications HTML5 et les navigateurs modernes la fixent à 4 millisecondes. "

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:

function testSettimeout0 () {
    var startTime = new Date().getTime()
    console.log('setting timeout 0 callback at ' +sinceStart())
    setTimeout(function(){
        console.log('in timeout callback at ' +sinceStart())
    }, 0)
    console.log('starting blocking loop at ' +sinceStart())
    while (sinceStart() < 3000) {
        continue
    }
    console.log('blocking loop ended at ' +sinceStart())
    return // functions below
    function sinceStart () {
        return new Date().getTime() - startTime
    } // sinceStart
} // testSettimeout0

La sortie est:

setting timeout 0 callback at 0
starting blocking loop at 5
blocking loop ended at 3000
in timeout callback at 3033
Vladimir Kornea
la source
votre réponse ne prouve rien. Cela montre simplement que sur votre machine sous dans une situation spécifique, l'ordinateur vous lance quelques chiffres. Pour prouver quelque chose de connexe, vous avez besoin d'un peu plus de quelques lignes de code et de quelques chiffres.
Salvador Dali
6
@SalvadorDali, je crois que ma preuve est suffisamment claire pour que la plupart des gens comprennent. Je pense que vous vous sentez défensif et n'avez pas fait l'effort de le comprendre. Je serai heureux d'essayer de le clarifier, mais je ne sais pas ce que vous ne comprenez pas. Essayez d'exécuter le code sur votre propre machine si vous suspectez mes résultats.
Vladimir Kornea
Je vais essayer de le préciser pour la dernière fois. Ma réponse clarifie certaines des propriétés intéressantes du délai d'attente, de l'intervalle et est bien prise en charge par des sources valides et reconnaissables. Vous avez affirmé fermement que c'était faux et vous avez indiqué votre morceau de code que, pour une raison quelconque, vous avez nommé une preuve. Il n'y a rien de mal avec votre morceau de code (je ne suis pas contre). Je dis simplement que ma réponse n'est pas fausse. Il clarifie certains points. Je vais arrêter cette guerre, car je ne vois rien.
Salvador Dali
15

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 de setTimeout(fn, 0)celui qui mérite d'être mentionné.

Pointu
la source
C'est précisément l'un des endroits où je l'ai vu utilisé. =)
Zaibot
Pour info: cette réponse a été fusionnée ici à partir de stackoverflow.com/questions/4574940/…
Shog9
2
consiste à reporter l'exécution du code à une boucle d'événements ultérieure distincte : comment déterminer une boucle d'événements suivante ? Comment déterminez-vous ce qu'est une boucle d'événements actuelle? Comment savez-vous à quel tour de boucle d'événement vous vous trouvez maintenant?
Vert
@Vert bien, vous ne le faites pas vraiment; il n'y a vraiment aucune visibilité directe sur ce que fait le runtime JavaScript.
Pointy
requestAnimationFrame a résolu le problème que j'avais avec IE et Firefox ne mettant pas à jour parfois l'interface utilisateur
David
9

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:

var f1 = function () {    
   setTimeout(function(){
      console.log("f1", "First function call...");
   }, 0);
};

var f2 = function () {
    console.log("f2", "Second call...");
};

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 setTimeoutavec 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.

Si le niveau d'imbrication est supérieur à 5 et que le délai d'attente est inférieur à 4, augmentez le délai d'attente à 4.

Aussi de mozilla:

Pour implémenter un délai d'expiration de 0 ms dans un navigateur moderne, vous pouvez utiliser window.postMessage () comme décrit ici .

Les informations PS sont prises après avoir lu l' article suivant .

Salvador Dali
la source
1
@ user2407309 Vous plaisantez? vous voulez dire que la spécification HTML5 est erronée et vous avez raison? Lisez les sources avant de voter vers le bas et faites de solides déclarations. Ma réponse est basée sur les spécifications HTML et sur l'historique. Au lieu de faire la réponse qui explique encore et encore la même chose, j'ai ajouté quelque chose de nouveau, quelque chose qui n'était pas montré dans les réponses précédentes. Je ne dis pas que c'est la seule raison, je montre juste quelque chose de nouveau.
Salvador Dali,
1
Ceci est incorrect: "Et voici pourquoi: il n'est pas possible d'avoir setTimeout avec un retard de 0 millisecondes." Ce n'est pas pour ça. Le retard de 4 ms est sans importance pour savoir pourquoi setTimeout(fn,0)est utile.
Vladimir Kornea
@ user2407309 il peut être facilement modifié en "pour ajouter aux raisons avancées par d'autres, ce n'est pas possible ....". Il est donc ridicule de voter à la baisse à cause de cela, surtout si votre propre réponse ne dit rien de nouveau. Un petit montage suffirait.
Salvador Dali
10
Salvador Dali: Si vous ignorez les aspects émotionnels de la guerre des micro-flammes ici, vous devrez probablement admettre que @VladimirKornea a raison. Il est vrai que les navigateurs mappent un retard de 0 ms à 4 ms, mais même s'ils ne le faisaient pas, les résultats seraient toujours les mêmes. Le mécanisme de conduite ici est que le code est poussé dans la file d'attente, plutôt que dans la pile d'appels. Jetez un œil à cette excellente présentation de JSConf, cela peut aider à clarifier le problème: youtube.com/watch?v=8aGhZQkoFbQ
hashchange
1
Je ne comprends pas pourquoi vous pensez que votre devis sur un minimum qualifié de 4 ms est un minimum global de 4 ms. Comme le montre votre citation de la spécification HTML5, le minimum est de 4 ms uniquement lorsque vous avez imbriqué des appels vers setTimeout/ setIntervalplus 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 (donc setIntervalavec 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.
ShadowRanger
8

Puisqu'il est passé une durée de 0, je suppose que c'est afin de supprimer le code passé au setTimeoutflux 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.

user113716
la source
Pour info: cette réponse a été fusionnée ici à partir de stackoverflow.com/questions/4574940/…
Shog9
7

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 #dobouton.

Ensuite, lorsque vous cliquez réellement sur le bouton, un messageest créé faisant référence à la fonction de gestionnaire d'événements, qui est ajoutée au message queue. Lorsque le event loopatteint ce message, il crée un framesur 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 onfonction 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 setTimeoutmarche 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 windowcontexte, 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 0ne "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 setTimeoutde 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 0pour 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.

DanielSmedegaardBuus
la source
3

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 whileboucle mais permet au moteur JavaScript de déclencher d'autres temporisateurs asynchrones.

Jason Suárez
la source
Votez pour cela push the function invocation to the bottom of the stack. Ce dont stackvous parlez est obscur. Le plus important est où exactement setTimeoutajoute cette fonction, fin de ce cycle de boucle ou tout début du cycle de boucle suivant.
Vert
1

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.

Jeremy
la source
1

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é.

fabspro
la source
2
La désactivation serait mieux effectuée dans l'événement onsubmit; ce serait plus rapide et il est garanti d'être appelé avant que le formulaire ne soit soumis techniquement puisque vous pouvez arrêter la soumission.
Kris
Très vrai. Je suppose que la désactivation onclick est plus facile pour le prototypage car vous pouvez simplement taper onclick = "this.disabled = true" dans le bouton tandis que la désactivation sur submit nécessite un peu plus de travail.
fabspro
1

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.

ChrisN
la source
2
Je suis un peu sceptique à propos de votre réponse, mais je l'ai appréciée car elle m'a obligé à faire des recherches supplémentaires sur les normes des navigateurs. Lorsque je recherche des normes, je vais là où je vais toujours, MDN: developer.mozilla.org/en-US/docs/Web/API/window.setTimeout La spécification HTML5 indique 4 ms. Il ne dit rien sur les limitations d'horloge des puces mobiles. Avoir du mal à rechercher une source d'informations pour sauvegarder vos déclarations. J'ai découvert que Dart Language by Google a supprimé setTimeout au profit d'un objet Timer.
John Zabroski
8
(...) parce que de nombreux navigateurs mobiles ne peuvent pas enregistrer des délais d'attente inférieurs à 20 millisecondes en raison des limitations d'horloge (...) Chaque plateforme a des limitations de timing en raison de son horloge et aucune plateforme n'est capable d'exécuter la chose suivante exactement 0 ms après la l'actuel. Le délai d'expiration de 0 ms demande l'exécution d'une fonction dès que possible et les limitations de temps d'une plate-forme spécifique ne changent en rien la signification de cela.
Piotr Dobrogost
1

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:

myObject.prototype.myMethodDeferred = function() {
    var deferredObject = $.Deferred();
    var that = this;  // Because setTimeout won't work right with this
    setTimeout(function() { 
        return myMethodActualWork.call(that, deferredObject);
    }, 0);
    return deferredObject.promise();
}
Stephan G
la source
1

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:

  1. 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 rappel setTimeout()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.
  2. La valeur 0 pour ms comme deuxième argument de la fonction 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 des setTimeout()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.
Willem van der Veen
la source
-1

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

Jani Devang
la source