web-dev-qa-db-fra.com

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

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

field.selectedIndex = element.index;

Cependant, ce code ne fonctionnait pas. Même si la variable selectedIndex du champ était correctement définie, le mauvais index serait sélectionné. Cependant, si je bloquais une déclaration alert() au bon moment, la bonne option serait sélectionnée. Pensant que cela pouvait ê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 à mon problème, mais je suis inquiet de ne pas savoir exactement pourquoi cela résout mon problème. Est-ce que quelqu'un a une explication officielle? Quel problème de navigateur ai-je évité en appelant ma fonction "plus tard" à l'aide de setTimeout()?

750
Daniel Lew

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

Un navigateur doit faire un certain nombre de choses en même temps, et une seule d'entre elles est l'exécution de JavaScript. Mais une des choses pour lesquelles JavaScript est très souvent utilisé est de demander au navigateur de construire un élément d’affichage. On suppose souvent que cela se fait de manière synchrone (notamment parce que JavaScript n'est pas exécuté en parallèle), mais rien ne garantit que c'est le cas et JavaScript ne dispose pas d'un mécanisme bien défini pour l'attente. 

La solution consiste à "suspendre" l'exécution de JavaScript pour laisser les threads de rendu rattraper leur retard. Et c’est l’effet que setTimeout() avec un délai d’attente de 0 fait. C'est comme un rendement fil/processus en C. Bien qu'il semble dire "lancez ceci immédiatement", il donne en fait au navigateur une chance de finir de faire des choses non-JavaScript qui attendaient de se terminer avant d'assister à cette nouvelle partie de JavaScript. .

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

IE6 se trouve être plus enclin à cette erreur, mais je l’ai vue se produire sur des versions plus anciennes de Mozilla et dans Firefox.


Voir Philip Roberts parler "Que diable est la boucle de l'événement?" pour une explication plus approfondie.

723
staticsan

Préface:

NOTE IMPORTANTE: Bien que la plupart des votes aient été acceptés et acceptés, la réponse acceptée par @staticsan est en fait PAS CORRECT! - voir le commentaire de David Mulder pour expliquer pourquoi.

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

En tant que tel, je publie une explication détaillée de ce que fait le navigateur et de la façon dont utiliser setTimeout() aide . Il a l'air long mais il est en réalité très simple et direct - je viens de le rendre très détaillé.

UPDATE: J'ai créé un JSFiddle pour démontrer en direct l'explication ci-dessous: http://jsfiddle.net/C2YBE/31/ . Merci beaucoup à @ThangChung pour avoir aidé à le relancer.

UPDATE2: 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 gestionnaire onClick pour "faire quelque chose" appelle une fonction "LongCalc ()", qui fait 2 choses:

  1. Fait un calcul très long (par exemple, prend 3 min)

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

Maintenant, vos utilisateurs commencent à tester ceci, cliquez sur le bouton "faire quelque chose", et la page reste là, ne faisant apparemment rien 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 gestionnaire onclick (fonction LongCalc()) pour effectuer 4 opérations:

  1. Remplissez le statut "Calcul en cours ... peut prendre environ 3 minutes" dans le statut DIV

  2. Fait un calcul très long (par exemple, prend 3 min)

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

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

Et, vous donnez volontiers l'application aux utilisateurs pour qu'ils testent à nouveau.

Ils vous reviennent avec l'air très en colère. Et expliquez-leur que, lorsqu'ils ont cliqué sur le bouton , la div de statut n'a jamais été mise à jour avec le statut "en cours de calcul ..." !!!


Vous vous grattez la tête, posez des questions sur StackOverflow (ou lisez des documents ou Google) et vous vous rendez compte du problème:

Le navigateur place toutes ses tâches "TODO" (tâches UI et commandes JavaScript) résultant d'événements dans une seule file d'attente . Et malheureusement, re-dessiner le DIV "Status" avec la nouvelle valeur "Calcul ..." est un TODO séparé qui va à la fin de la file d'attente!

Voici une ventilation des événements lors du test de votre utilisateur, le contenu de la file d'attente après chaque événement:

  • File d'attente: [Empty]
  • Evénement: Cliquez sur le bouton. File d'attente après l'événement: [Execute OnClick handler(lines 1-4)]
  • Événement: Exécutez la première ligne du gestionnaire OnClick (par exemple, changez la valeur Status DIV). File d'attente après événement: [Execute OnClick handler(lines 2-4), re-draw Status DIV with new "Calculating" value]. Veuillez noter que, même si les modifications du DOM sont instantanées, vous avez besoin d'un nouvel événement pour déclencher à nouveau l'élément DOM correspondant, déclenché par la modification du DOM et placé à la fin de la file d'attente .
  • PROBLÈME !!! PROBLÈME !!! Les détails sont expliqués ci-dessous.
  • Evénement: Exécute 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].
  • Evénement: Exécuter la 3ème ligne dans le gestionnaire (peupler 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].
  • Evénement: Exécute la 4ème ligne dans le gestionnaire (remplissez le statut DIV avec "DONE"). File d'attente: [Execute OnClick handler, re-draw Status DIV with "Calculating" value, re-draw result DIV with result; re-draw Status DIV with "DONE" value].
  • Evénement: exécuter implicite return à partir de onclick gestionnaire secondaire. Nous retirons le "gestionnaire OnClick Execute" de la file d'attente et commençons à exécuter l'élément suivant de la file d'attente.
  • NOTE: Puisque nous avons déjà fini le calcul, 3 minutes ont déjà été passées pour l'utilisateur. L'événement de re-tirage n'a pas encore eu lieu !!!
  • Evénement: redessinez Status DIV avec la valeur "Calculating". Nous faisons le redécoupage et le retirons de la file d'attente.
  • Evénement: redessinez le résultat DIV avec la valeur du résultat. Nous faisons le redécoupage et le retirons de la file d'attente.
  • Evénement: redessinez Status DIV avec la valeur "Done". Nous faisons le redécoupage et le retirons de la file d'attente. Les spectateurs attentifs pourraient même remarquer "Status DIV" avec une valeur "Calculating" clignotant pour une fraction de microseconde - APRÈS LE CALCUL FINI

Le problème sous-jacent est donc que l'événement de redessinage de "Status" DIV est placé dans la file d'attente à la fin, APRÈS l'événement "execute line 2" qui prend 3 minutes, de sorte que le re-dessin réel ne se produit pas avant APRÈS le calcul est terminé.


À la rescousse, la setTimeout(). Comment ça aide? En appelant du code à longue exécution via setTimeout, vous créez en fait 2 événements: setTimeout l'exécution elle-même et (en raison de la temporisation égale à 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 gestionnaire onClick en deux instructions (dans une nouvelle fonction ou tout simplement un bloc dans onClick):

  1. Remplissez le statut "Calcul en cours ... peut prendre environ 3 minutes" dans le statut DIV

  2. Exécutez setTimeout() avec un délai d'attente de 0 et un appel à la fonction LongCalc().

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

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

  • File d'attente: [Empty]
  • Evénement: Cliquez sur le bouton. File d'attente après l'événement: [Execute OnClick handler(status update, setTimeout() call)]
  • Événement: Exécutez la première ligne du gestionnaire OnClick (par exemple, changez la valeur Status DIV). File d'attente après événement: [Execute OnClick handler(which is a setTimeout call), re-draw Status DIV with new "Calculating" value].
  • Evé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 dedans pendant 0 secondes supplémentaires.
  • Evénement: l'alarme de la temporisation s'éteint, 0 seconde plus tard. File d'attente après: [re-draw Status DIV with "Calculating" value, execute LongCalc (lines 1-3)].
  • Evénement: redessinez la division Status DIV avec la valeur "Calculating" . File d'attente après: [execute LongCalc (lines 1-3)]. Veuillez noter que cet événement de re-dessin pourrait effectivement se produire AVANT que l'alarme se déclenche, ce qui fonctionne aussi bien.
  • ...

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



Voici l'exemple de code de 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 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);
});
627
DVK

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

85
Andy

La plupart des navigateurs ont un processus appelé thread principal, chargé d'exécuter certaines tâches JavaScript, mises à jour d'interface utilisateur, par exemple: peinture, redessinage ou redistribution, etc.

Certaines tâches d'exécution de 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 à exécuter.

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 à la file d'attente des messages.

setTimeout(fn, 0); ajoute cette fn à la fin de la file d'attente à exécuter . Elle planifie l'ajout d'une tâche à la file d'attente après une durée donnée.

21
arley

setTimeout() vous permet de gagner du temps avant le chargement des éléments DOM, même s'il est défini sur 0.

Découvrez ceci: setTimeout

20
Jose Basilio

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

"Et voici pourquoi: il n'est pas possible d'avoir setTimeout avec un délai De 0 millisecondes. La valeur Minimum est déterminée par le navigateur Et non de 0 millisecondes. Historiquement, les navigateurs définissent cette valeur. 10 millisecondes, mais les spécifications HTML5 et les navigateurs modernes sont réglés à 4 millisecondes. "

Le délai d'expiration minimum de 4 ms n'a aucune incidence sur 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 dont l'exécution prend plusieurs secondes, le rappel ne sera pas exécuté pendant plusieurs secondes, jusqu'à la fin du code de blocage. 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
18
Vladimir Kornea

Une des raisons est de différer l’exécution du code dans une boucle d’événement ultérieure et distincte. Lorsque vous répondez à un événement du navigateur (clic de souris, par exemple), il est parfois nécessaire de n'effectuer que des opérations après l'événement en cours est traité. La fonction setTimeout() est le moyen le plus simple de le faire.

edit maintenant que nous sommes en 2015, je dois noter qu'il y a aussi requestAnimationFrame(), qui n'est pas exactement identique, mais suffisamment proche de setTimeout(fn, 0) pour que cela mérite d'être mentionné.

13
Pointy

C'est une vieille question avec de vieilles réponses. Je voulais ajouter un nouveau regard sur ce problème et expliquer 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(); pour voir que le second s’exécute en premier. 

Et voici pourquoi: il n’est pas possible d’avoir setTimeout avec un délai de 0 millisecondes. La valeur minimum est déterminée par le navigateur et ne correspond pas à 0 millisecondes. Historiquement les navigateurs définissent 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, alors augmente le délai d'attente à 4.

Aussi de mozilla:

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

P.S. les informations sont prises après avoir lu le article .

9
Salvador Dali

Puisqu'il est passé une durée de 0, je suppose que c'est pour supprimer le code transmis à setTimeout du flux d'exécution. Donc, si c'est une fonction qui peut prendre un certain temps, cela n'empêchera pas le code suivant de s'exécuter.

8
user113716

L'autre chose que cela fait est de pousser l'appel de fonction au bas de la pile, empêchant ainsi un débordement de pile si vous appelez une fonction de manière récursive. Cela a pour effet une boucle while mais permet au moteur JavaScript de déclencher d'autres minuteurs asynchrones.

3
Jason Suárez

Les réponses concernant les boucles d'exécution et le rendu du DOM avant la fin du code sont correctes. L’aide de JavaScript rend le code pseudo-multithread, même s’il ne l’est pas.

Je souhaite ajouter que la meilleure valeur pour un délai d'attente instantané inter-plates-formes/navigateur multiplate-forme en JavaScript est d'environ 20 millisecondes au lieu de 0 (zéro), car de nombreux navigateurs mobiles ne peuvent pas enregistrer des délais d'expiration inférieurs à 20 millisecondes en raison de limitations d'horloge sur les puces AMD.

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

2
ChrisN

Quelques autres cas où setTimeout est utile:

Vous souhaitez décomposer une boucle ou un calcul de longue durée en composants plus petits afin que le navigateur ne semble pas "figer" ni dire "Script sur la page 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 égal à zéro fait l'affaire, permettant à l'événement de se terminer, au formulaire de commencer à envoyer, puis votre bouton peut être désactivé.

1
fabspro

Le problème était que vous essayiez d'exécuter 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() provoque l'événement à être ansynchronous et est donc exécuté après tout le code synchrone, ce qui laisse plus de temps à votre élément pour se charger. Les rappels asynchrones comme le rappel dans setTimeout() sont placés dans la file d'attente d'événements et placés sur la pile par la boucle d'événement après que la pile de code synchrone soit vide. 
  2. La valeur 0 pour ms en tant que second argument de la fonction setTimeout() est souvent légèrement supérieure (4 à 10 ms selon le navigateur). Ce temps légèrement plus long nécessaire pour exécuter les rappels setTimeout() est provoqué par le nombre de "ticks" (lorsqu'un tick cale un rappel sur la pile si la pile est vide) de la boucle d'événement. Pour des raisons de performances et de durée de vie de la batterie, le nombre de ticks dans la boucle d’événements est limité à un certain nombre de moins à 1000 fois par seconde.
1
Willem van der Veen

setTimeout de 0 est également très utile dans la configuration de la promesse différée, que vous souhaitez retourner tout de suite:

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();
}
1
Stephan G

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. 

1
Jeremy

Javascript est une application à un seul thread, il est donc interdit d’exécuter des fonctions simultanément. Les boucles sont donc utilisées. Ainsi, exactement ce que setTimeout (fn, 0) fait, il est poussé dans une quête de tâche qui est exécutée lorsque votre pile d’appels est vide. Je sais que cette explication est assez ennuyeuse, je vous recommande donc de visionner cette vidéo qui vous aidera à comprendre comment les choses se passent sous le capot du navigateur . Regardez cette vidéo: - https://www.youtube.com/watch? time_continue = 392 & v = 8aGhZQkoFbQ

0
Jani Devang