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


J'ai récemment rencontré un bug plutôt méchant, dans lequel le code chargeait un <select> dynamiquement via JavaScript. Ceci dynamiquement chargé <select> avait une valeur présélectionnée. Dans IE6, nous avions déjà du code pour corriger le <option>, parce que parfois le <select>de selectedIndex la valeur serait désynchronisée avec la valeur sélectionnée <option>de index attribut, comme ci-dessous:

field.selectedIndex = element.index;

Cependant, ce code ne fonctionnait pas. Même si le domaine selectedIndex était réglé correctement, le mauvais index finirait par être sélectionné. Cependant, si je bloque un 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 hasard que j'avais déjà vu 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 pour mon problème, mais je ne suis pas à l'aise que je ne sais pas exactement pourquoi cela résout mon problème. Quelqu'un at-il une explication officielle? Quelle est la question de navigateur que j'évite en appelant ma fonction "plus tard" en utilisant setTimeout()?


703
2018-04-22 21:46


origine


Réponses:


Cela fonctionne parce que vous faites du multi-tâches en coopération.

Un navigateur doit faire un certain nombre de choses à la fois, et l'un d'entre eux est l'exécution de JavaScript. Mais une des choses que JavaScript utilise très souvent est de demander au navigateur de construire un élément d'affichage. Ceci est souvent supposé être fait de manière synchrone (d'autant plus que JavaScript n'est pas exécuté en parallèle) mais il n'y a aucune garantie que c'est le cas et JavaScript n'a pas de mécanisme d'attente bien défini.

La solution consiste à "mettre en pause" l'exécution de JavaScript pour laisser les threads de rendu se rattraper. Et c'est l'effet que setTimeout() avec un délai d'attente de 0 Est-ce que. C'est un peu comme un fil / processus en C. Bien qu'il semble dire "lancer ceci immédiatement" cela donne au navigateur une chance de finir de faire des choses non JavaScript qui attendaient de finir avant de s'occuper de ce nouveau morceau de JavaScript .

(En réalité, setTimeout() remet en file d'attente le nouveau code JavaScript à la fin de la file d'attente d'exécution. Voir les commentaires pour les liens vers une explication plus longue.)

IE6 arrive juste à être plus enclin à cette erreur, mais je l'ai vu se produire sur les anciennes versions de Mozilla et Firefox.


666
2018-04-23 00:14



Préface:

NOTE IMPORTANTE: Bien qu'il soit le plus élevé et accepté, la réponse acceptée par @staticsan est en fait PAS CORRECTE! - Voir le commentaire de David Mulder pour expliquer pourquoi.

Certaines des autres réponses sont correctes mais n'illustrent pas vraiment le problème à résoudre. J'ai donc créé cette réponse pour présenter cette illustration détaillée.

En tant que tel, je poste un détaillée de ce que fait le navigateur et comment utiliser setTimeout() aide. Il semble assez long mais est en fait très simple et direct - je l'ai juste fait très détaillé.

METTRE À JOUR: J'ai fait un JSFiddle à vivre-démontrer l'explication ci-dessous: http://jsfiddle.net/C2YBE/31/ . Beaucoup Merci à @ ChangChung pour avoir aidé à le lancer.

UPDATE2: Juste au cas où le site Web JSFiddle meurt, ou supprime 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 pour le bouton "faire quelque chose" appelle une fonction "LongCalc ()", qui fait 2 choses:

  1. Fait un très long calcul (dis prend 3 min)

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

Maintenant, vos utilisateurs commencent à tester cela, cliquez sur "faire quelque chose" bouton, et la page se trouve là en faisant apparemment rien pendant 3 minutes, ils deviennent agités, cliquez à nouveau sur le bouton, attendez 1 minute, rien ne se passe, cliquez sur le bouton à nouveau ...

Le problème est évident - vous voulez un DIV "Status", qui montre ce qui se passe. Voyons voir comment cela fonctionne.


Donc vous ajoutez un DIV "Statut" (initialement vide), et modifiez le onclick gestionnaire (fonction LongCalc()) pour faire 4 choses:

  1. Remplir le statut "Calculer ... peut prendre ~ 3 minutes" en statut DIV

  2. Fait un très long calcul (dis prend 3 min)

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

  4. Remplir le statut "Calcul effectué" en statut DIV

Et, vous donnez heureusement l'application aux utilisateurs de re-tester.

Ils vous reviennent très en colère. Et explique que quand ils ont cliqué sur le bouton, le statut DIV n'a jamais été mis à jour avec le statut "Calculer ..." !!!


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" (les tâches de l'interface utilisateur et les commandes JavaScript) résultant des événements dans un file d'attente unique. Et malheureusement, redessiner le DIV "Status" avec la nouvelle valeur "Calculating ..." est un TODO séparé qui va à la fin de la file d'attente!

Voici une répartition 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 événement: [Execute OnClick handler(lines 1-4)]
  • Evénement: Exécuter la première ligne dans le gestionnaire OnClick (par exemple modifier la valeur DIV du statut). File d'attente après événement: [Execute OnClick handler(lines 2-4), re-draw Status DIV with new "Calculating" value]. S'il vous plaît noter que tandis 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 est allé à la fin de la file d'attente.
  • PROBLÈME!!!  PROBLÈME!!! Détails expliqués ci-dessous.
  • Événement: Exécuter la deuxième ligne dans le 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écuter la 4ème ligne dans le gestionnaire (remplir 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écuter implicite return de onclick gestionnaire sous. Nous prenons le "Execute OnClick handler" hors de la file d'attente et commençons à exécuter l'élément suivant dans la file d'attente.
  • NOTE: Puisque nous avons déjà terminé le calcul, 3 minutes sont déjà passées pour l'utilisateur. L'événement de re-draw n'est pas encore arrivé !!!
  • Événement: redessinez Status DIV avec la valeur "Calculating". Nous faisons le re-draw et l'enlève de la file d'attente.
  • Événement: redessinez le résultat DIV avec la valeur du résultat. Nous faisons le re-draw et l'enlève de la file d'attente.
  • Événement: redessinez Status DIV avec la valeur "Done". Nous faisons le re-draw et l'enlève de la file d'attente. Les téléspectateurs aux yeux pointus pourraient même remarquer "Statut DIV avec" Valeur de calcul "clignotant pour une fraction de microseconde - APRES LE CALCUL FINI

Ainsi, le problème sous-jacent est que l'événement de re-draw pour "Status" DIV est placé dans la queue à la fin, APRÈS l'événement "execute line 2" qui dure 3 minutes, donc le re-draw ne se produit que APRÈS le calcul est fait.


À la rescousse vient le setTimeout(). Comment ça aide? Parce qu'en appelant le code à exécution longue via setTimeout, vous créez réellement 2 événements: setTimeout exécution elle-même, et (en raison de 0 timeout), entrée de file d'attente séparée pour le code en cours d'exécution.

Donc, pour résoudre votre problème, vous modifiez votre onClick gestionnaire pour être deux instructions (dans une nouvelle fonction ou juste un bloc dans onClick):

  1. Remplir le statut "Calculer ... peut prendre ~ 3 minutes" en statut DIV

  2. Exécuter setTimeout() avec 0 timeout et un appel à LongCalc() fonction.

    LongCalc() la fonction est presque la même que la dernière fois mais n'a évidemment pas de statut "Calculer ..." DIV mise à jour en tant que première étape; et au lieu de commencer le calcul tout de suite.

Alors, à quoi ressemble la séquence d'événements et la file d'attente?

  • Queue: [Empty]
  • Événement: Cliquez sur le bouton. File d'attente après événement: [Execute OnClick handler(status update, setTimeout() call)]
  • Evénement: Exécuter la première ligne dans le gestionnaire OnClick (par exemple modifier la valeur DIV du statut). File d'attente après événement: [Execute OnClick handler(which is a setTimeout call), re-draw Status DIV with new "Calculating" value].
  • Événement: Exécute 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 du délai expire 0 seconde plus tard. File d'attente après: [re-draw Status DIV with "Calculating" value, execute LongCalc (lines 1-3)].
  • Un événement: redessinez le statut DIV avec la valeur "Calculer". File d'attente après: [execute LongCalc (lines 1-3)]. S'il vous plaît noter que cet événement de re-tirage pourrait réellement se produire AVANT que l'alarme se déclenche, ce qui fonctionne tout aussi bien.
  • ...

Hourra! Le statut DIV vient d'être mis à jour pour "Calculer ..." avant le début du calcul !!!



Voici 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é le onDomReady et 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);
});

558
2018-01-01 17:53



Jetez un oeil à l'article de John Resig sur Comment fonctionnent les minuteurs JavaScript. Lorsque vous définissez un délai d'attente, il met en file d'attente le code asynchrone jusqu'à ce que le moteur exécute la pile d'appels en cours.


82
2017-12-06 21:38



La plupart des navigateurs ont un processus appelé thread principal, qui est responsable de l'exécution de certaines tâches JavaScript, des mises à jour de l'interface utilisateur, par exemple: peindre, redessiner ou redistribuer, 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 distribuées dans le thread principal du navigateur à exécuter.

Lorsque les 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 des messages.

setTimeout(fn, 0); Ajoute ça 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.


20
2018-04-22 21:52



setTimeout() vous achète un peu de temps jusqu'à ce que les éléments DOM soient chargés, même si la valeur est 0.

Regarde ça: setTimeout


19
2017-07-26 00:30



Il y a des réponses contestées contradictoires ici, et sans preuve il n'y a aucun moyen de savoir à qui croire. Voici la preuve que @DVK est correct et @SalvadorDali est faux. Ce dernier affirme:

"Et voici pourquoi: il n'est pas possible d'avoir setTimeout avec un temps   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   faites-le à 4 millisecondes. "

Le délai minimum de 4 ms n'est pas pertinent pour ce qui se passe. Ce qui se passe réellement, c'est que setTimeout pousse la fonction de rappel à la fin de la file d'attente d'exécution. Si, après setTimeout (callback, 0) vous avez un code de blocage qui dure plusieurs secondes, le callback ne sera pas exécuté pendant plusieurs secondes, tant que le code de blocage n'est pas 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

15
2018-01-01 17:38



Une raison à cela est de reporter l'exécution du code à une boucle d'événement ultérieure distincte. Lorsque vous répondez à un événement de navigateur (clic de souris, par exemple), il est parfois nécessaire d'effectuer uniquement des opérations après l'événement en cours est traité. le setTimeout() installation est le moyen le plus simple de le faire.

modifier maintenant que c'est 2015, je dois noter qu'il y a aussi requestAnimationFrame(), ce qui n'est pas exactement la même chose mais suffisamment proche de setTimeout(fn, 0) que ça vaut la peine de mentionner.


12
2018-05-19 21:34



C'est une vieille question avec de vieilles réponses. Je voulais ajouter un nouveau regard sur ce problème et répondre à la question de savoir pourquoi cela se produit et non pourquoi cela est utile.

Donc vous avez 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(); juste pour voir que le second a été exécuté en premier.

Et voici pourquoi: il n'est pas possible d'avoir setTimeout avec un retard de 0 millisecondes. le La valeur minimale est déterminée par le navigateur et ce n'est pas 0 millisecondes. Historiquement navigateurs définit ce minimum à 10 millisecondes, mais le Spécifications HTML5 et les navigateurs modernes l'ont mis à 4 millisecondes.

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

Aussi de mozilla:

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

P.S. l'information est prise après avoir lu ce qui suit article.


9
2018-01-01 17:39



Comme il est passé une durée de 0, Je suppose que c'est pour supprimer le code transmis au setTimeout du flux de l'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.


8
2018-06-21 01:14



L'autre chose que cela fait est de pousser l'invocation de fonction au fond de la pile, empêchant un débordement de pile si vous appelez une fonction récursivement. Cela a l'effet d'un while loop mais permet au moteur JavaScript de déclencher d'autres minuteurs asynchrones.


3
2018-03-13 23:29



Les réponses à propos des boucles d'exécution et du rendu du DOM avant la fin d'un autre code sont correctes. Zero secondes délais en JavaScript aident à rendre le code pseudo-multithread, même si ce n'est pas le cas.

Je tiens à ajouter que la valeur BEST pour un timeout cross-browser / cross-plate-forme en JavaScript est en réalité de 20 millisecondes au lieu de 0 (zéro), car de nombreux navigateurs mobiles ne peuvent pas enregistrer des délais inférieurs à 20 millisecondes sur les puces AMD.

En outre, les processus de longue durée qui n'impliquent pas la manipulation DOM doivent être envoyés aux Web Workers maintenant, car ils fournissent une véritable exécution multithread de JavaScript.


2
2018-04-22 21:53