Warum ist setTimeout (fn, 0) manchmal nützlich?


872

Ich bin kürzlich auf einen ziemlich bösen Fehler gestoßen, bei dem der Code <select>dynamisch über JavaScript geladen wurde. Dieses dynamisch geladene <select>hatte einen vorgewählten Wert. In IE6, wir Code bereits hatten die ausgewählt zu beheben <option>, weil manchmal der <select>‚s selectedIndexwürde Wert synchron aus dem ausgewählten <option>‘ s indexAttribute, wie folgt:

field.selectedIndex = element.index;

Dieser Code funktionierte jedoch nicht. Obwohl die Felder selectedIndexkorrekt eingestellt wurden, wurde am Ende der falsche Index ausgewählt. Wenn ich jedoch eine alert()Aussage zum richtigen Zeitpunkt einfügte, wurde die richtige Option ausgewählt. Da ich dachte, dies könnte ein Timing-Problem sein, versuchte ich etwas Zufälliges, das ich zuvor im Code gesehen hatte:

var wrapFn = (function() {
    var myField = field;
    var myElement = element;

    return function() {
        myField.selectedIndex = myElement.index;
    }
})();
setTimeout(wrapFn, 0);

Und das hat funktioniert!

Ich habe eine Lösung für mein Problem, aber es ist mir unangenehm, dass ich nicht genau weiß, warum dies mein Problem behebt. Hat jemand eine offizielle Erklärung? Welches Browserproblem vermeide ich, indem ich meine Funktion "später" mit aufrufe setTimeout()?


2
Die meisten Fragen beschreiben, warum es nützlich ist. Wenn Sie wissen müssen, warum dies passiert - lesen Sie meine Antwort: stackoverflow.com/a/23747597/1090562
Salvador Dali

18
Philip Roberts erklärt dies hier in seinem Vortrag "Was zum Teufel ist die Ereignisschleife?" youtube.com/watch?v=8aGhZQkoFbQ
Vasa

Wenn Sie es eilig haben, ist dies der Teil des Videos, in dem er die Frage genau anspricht : youtu.be/8aGhZQkoFbQ?t=14m54s . Trotzdem ist das ganze Video auf jeden Fall eine Uhr wert.
William Hampshire

4
setTimeout(fn)ist setTimeout(fn, 0)übrigens dasselbe wie .
vsync

Antworten:


827

In der Frage bestand eine Rassenbedingung zwischen:

  1. Der Versuch des Browsers, die Dropdown-Liste zu initialisieren, damit der ausgewählte Index aktualisiert werden kann, und
  2. Ihr Code zum Festlegen des ausgewählten Index

Ihr Code hat dieses Rennen durchweg gewonnen und versucht, die Dropdown-Auswahl festzulegen, bevor der Browser bereit war, was bedeutet, dass der Fehler auftreten würde.

Diese Rasse bestand, weil JavaScript einen einzigen Ausführungsthread hat , der für das Rendern von Seiten freigegeben ist. Tatsächlich blockiert das Ausführen von JavaScript die Aktualisierung des DOM.

Ihre Problemumgehung war:

setTimeout(callback, 0)

Wenn Sie setTimeoutmit einem Rückruf und Null als zweitem Argument aufrufen, wird der Rückruf nach der kürzestmöglichen Verzögerung asynchron ausgeführt. Dies liegt bei etwa 10 ms, wenn die Registerkarte den Fokus hat und der JavaScript-Ausführungsthread nicht belegt ist.

Die Lösung des OP bestand daher darin, die Einstellung des ausgewählten Index um etwa 10 ms zu verzögern. Dies gab dem Browser die Möglichkeit, das DOM zu initialisieren und den Fehler zu beheben.

Jede Version von Internet Explorer zeigte ein eigenartiges Verhalten, und diese Art der Problemumgehung war zeitweise erforderlich. Alternativ könnte es sich um einen echten Fehler in der Codebasis des OP handeln.


Sehen Sie, wie Philip Roberts spricht: "Was zum Teufel ist die Ereignisschleife?" für eine gründlichere Erklärung.


276
"Die Lösung besteht darin, die JavaScript-Ausführung anzuhalten, damit die Rendering-Threads aufholen." SetTimeout ist nicht ganz richtig. Es fügt der Browser-Ereigniswarteschlange ein neues Ereignis hinzu, und die Rendering-Engine befindet sich bereits in dieser Warteschlange (nicht ganz wahr, aber nahe genug), sodass sie vor dem setTimeout-Ereignis ausgeführt wird.
David Mulder

48
Ja, das ist eine viel detailliertere und korrektere Antwort. Aber meine ist "richtig genug", damit die Leute verstehen, warum der Trick funktioniert.
Statik

2
@DavidMulder, bedeutet dies, dass der Browser CSS analysiert und in einem anderen Thread als dem JavaScript-Ausführungsthread rendert?
Jason

8
Nein, sie werden im Prinzip im selben Thread analysiert, da sonst einige Zeilen der DOM-Manipulation ständig Rückflüsse auslösen würden, was einen extrem schlechten Einfluss auf die Ausführungsgeschwindigkeit hätte.
David Mulder

30
Dieses Video ist die beste Erklärung dafür, warum wir Timeout 0 2014 eingestellt haben. Jsconf.eu/speakers/…
davibq

665

Vorwort:

Einige der anderen Antworten sind korrekt, veranschaulichen jedoch nicht das zu lösende Problem. Daher habe ich diese Antwort erstellt, um diese detaillierte Abbildung zu präsentieren.

Aus diesem Grund veröffentliche ich eine detaillierte Anleitung, was der Browser tut und wie die Verwendung setTimeout()hilft . Es sieht länger aus, ist aber eigentlich sehr einfach und unkompliziert - ich habe es nur sehr detailliert gemacht.

UPDATE: Ich habe eine JSFiddle erstellt, um die folgende Erklärung live zu demonstrieren: http://jsfiddle.net/C2YBE/31/ . Vielen Dank an @ThangChung für die Unterstützung beim Kickstart.

UPDATE2: Nur für den Fall, dass die JSFiddle-Website stirbt oder den Code löscht, habe ich den Code ganz am Ende zu dieser Antwort hinzugefügt.


DETAILS :

Stellen Sie sich eine Web-App mit einer Schaltfläche "Etwas tun" und einem Ergebnis-Div vor.

Der onClickHandler für die Schaltfläche "etwas tun" ruft eine Funktion "LongCalc ()" auf, die zwei Dinge ausführt:

  1. Macht eine sehr lange Berechnung (dauert etwa 3 Minuten)

  2. Druckt die Berechnungsergebnisse in das Ergebnis div.

Jetzt beginnen Ihre Benutzer, dies zu testen, klicken auf die Schaltfläche "etwas tun", und die Seite befindet sich dort und tut 3 Minuten lang scheinbar nichts. Sie werden unruhig. Klicken Sie erneut auf die Schaltfläche. Warten Sie 1 Minute. Es passiert nichts. Klicken Sie erneut auf die Schaltfläche.

Das Problem liegt auf der Hand - Sie möchten einen "Status" -DIV, der zeigt, was los ist. Mal sehen, wie das funktioniert.


Sie fügen also einen "Status" -DIV (anfangs leer) hinzu und ändern den onclickHandler (Funktion LongCalc()), um 4 Dinge zu tun:

  1. Füllen Sie den Status "Berechnen ... kann ~ 3 Minuten dauern" in den Status DIV ein

  2. Macht eine sehr lange Berechnung (dauert etwa 3 Minuten)

  3. Druckt die Berechnungsergebnisse in das Ergebnis div.

  4. Füllen Sie den Status "Berechnung erledigt" in den Status DIV

Und Sie geben die App gerne an Benutzer weiter, um sie erneut zu testen.

Sie kommen zu dir zurück und sehen sehr wütend aus. Und erklären Sie, dass der Status DIV beim Klicken auf die Schaltfläche nie mit dem Status "Berechnen ..." aktualisiert wurde !!!


Sie kratzen sich am Kopf, fragen bei StackOverflow nach (oder lesen Dokumente oder Google) und erkennen das Problem:

Der Browser platziert alle "TODO" -Aufgaben (sowohl UI-Aufgaben als auch JavaScript-Befehle), die sich aus Ereignissen ergeben, in einer einzigen Warteschlange . Und leider ist das erneute Zeichnen des DIV "Status" mit dem neuen Wert "Berechnen ..." ein separates TODO, das bis zum Ende der Warteschlange reicht!

Hier ist eine Aufschlüsselung der Ereignisse während des Tests Ihres Benutzers sowie der Inhalt der Warteschlange nach jedem Ereignis:

  • Warteschlange: [Empty]
  • Ereignis: Klicken Sie auf die Schaltfläche. Warteschlange nach Ereignis:[Execute OnClick handler(lines 1-4)]
  • Ereignis: Führen Sie die erste Zeile im OnClick-Handler aus (z. B. Status DIV-Wert ändern). Warteschlange nach Ereignis : [Execute OnClick handler(lines 2-4), re-draw Status DIV with new "Calculating" value]. Beachten Sie, dass Sie, während die DOM-Änderungen sofort erfolgen, zum erneuten Zeichnen des entsprechenden DOM-Elements ein neues Ereignis benötigen, das durch die DOM-Änderung am Ende der Warteschlange ausgelöst wird .
  • PROBLEM!!! PROBLEM!!! Details unten erklärt.
  • Ereignis: Führen Sie die zweite Zeile im Handler aus (Berechnung). Warteschlange nach : [Execute OnClick handler(lines 3-4), re-draw Status DIV with "Calculating" value].
  • Ereignis: Führen Sie die dritte Zeile im Handler aus (füllen Sie das Ergebnis DIV aus). Warteschlange nach : [Execute OnClick handler(line 4), re-draw Status DIV with "Calculating" value, re-draw result DIV with result].
  • Ereignis: Führen Sie die 4. Zeile im Handler aus (füllen Sie den Status DIV mit "DONE"). Warteschlange : [Execute OnClick handler, re-draw Status DIV with "Calculating" value, re-draw result DIV with result; re-draw Status DIV with "DONE" value].
  • Ereignis: Ausführen implizit returnvom onclickHandler-Sub. Wir nehmen den "Execute OnClick-Handler" aus der Warteschlange und beginnen mit der Ausführung des nächsten Elements in der Warteschlange.
  • HINWEIS: Da wir die Berechnung bereits abgeschlossen haben, sind für den Benutzer bereits 3 Minuten vergangen. Das Re-Draw-Event hat noch nicht stattgefunden !!!
  • Ereignis: Status DIV mit dem Wert "Berechnen" neu zeichnen. Wir zeichnen neu und nehmen das aus der Warteschlange.
  • Ereignis: Ergebnis-DIV mit Ergebniswert neu zeichnen. Wir zeichnen neu und nehmen das aus der Warteschlange.
  • Ereignis: Status DIV mit dem Wert "Fertig" neu zeichnen. Wir zeichnen neu und nehmen das aus der Warteschlange. Betrachter mit scharfen Augen bemerken möglicherweise sogar "Status DIV", wobei der Wert "Berechnen" für den Bruchteil einer Mikrosekunde blinkt - NACH BEENDETER BERECHNUNG

Das zugrunde liegende Problem besteht also darin, dass das Ereignis zum erneuten Zeichnen für "Status" DIV am Ende in die Warteschlange gestellt wird, NACH dem Ereignis "Zeile 2 ausführen", das 3 Minuten dauert, sodass das eigentliche erneute Zeichnen erst erfolgt Nachdem die Berechnung abgeschlossen ist.


Zur Rettung kommt der setTimeout(). Wie hilft es? Denn durch das Aufrufen von Code mit langer Ausführung über setTimeouterstellen Sie tatsächlich zwei Ereignisse: die setTimeoutAusführung selbst und (aufgrund eines Zeitlimits von 0) einen separaten Warteschlangeneintrag für den ausgeführten Code.

Um Ihr Problem zu beheben, ändern Sie Ihren onClickHandler in ZWEI Anweisungen (in einer neuen Funktion oder nur in einem Block innerhalb onClick):

  1. Füllen Sie den Status "Berechnen ... kann ~ 3 Minuten dauern" in den Status DIV ein

  2. Führen setTimeout()mit 0 Timeout und einen Aufruf LongCalc()Funktion .

    LongCalc()Funktion ist fast die gleiche wie beim letzten Mal, hat aber offensichtlich nicht den Status "Berechnen ..." DIV-Update als ersten Schritt; und startet stattdessen sofort die Berechnung.

Wie sehen die Ereignissequenz und die Warteschlange jetzt aus?

  • Warteschlange: [Empty]
  • Ereignis: Klicken Sie auf die Schaltfläche. Warteschlange nach Ereignis:[Execute OnClick handler(status update, setTimeout() call)]
  • Ereignis: Führen Sie die erste Zeile im OnClick-Handler aus (z. B. Status DIV-Wert ändern). Warteschlange nach Ereignis : [Execute OnClick handler(which is a setTimeout call), re-draw Status DIV with new "Calculating" value].
  • Ereignis: Führen Sie die zweite Zeile im Handler aus (setTimeout-Aufruf). Warteschlange nach : [re-draw Status DIV with "Calculating" value]. Die Warteschlange enthält noch 0 Sekunden lang nichts Neues.
  • Ereignis: Der Alarm aus dem Timeout wird 0 Sekunden später ausgelöst. Warteschlange nach : [re-draw Status DIV with "Calculating" value, execute LongCalc (lines 1-3)].
  • Ereignis: Status DIV mit dem Wert "Berechnen" neu zeichnen . Warteschlange nach : [execute LongCalc (lines 1-3)]. Bitte beachten Sie, dass dieses Ereignis zum erneuten Zeichnen tatsächlich eintreten kann, BEVOR der Alarm ausgelöst wird. Dies funktioniert genauso gut.
  • ...

Hurra! Der Status DIV wurde gerade vor Beginn der Berechnung auf "Berechnen ..." aktualisiert !!!



Unten finden Sie den Beispielcode von JSFiddle, der diese Beispiele veranschaulicht: http://jsfiddle.net/C2YBE/31/ :

HTML Quelltext:

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

JavaScript-Code: (Wird ausgeführt onDomReadyund erfordert möglicherweise 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);
});

12
tolle Antwort DVK! Hier ist eine Übersicht, die Ihr Beispiel veranschaulicht. Gist.github.com/kumikoda/5552511#file-timeout-html
kumikoda

4
Wirklich coole Antwort, DVK. Um es mir leicht vorzustellen, habe ich diesen Code in jsfiddle jsfiddle.net/thangchung/LVAaV
thangchung

4
@ThangChung - Ich habe versucht, eine bessere Version (2 Schaltflächen, jeweils eine für jeden Fall) in JSFiddle zu erstellen. Es funktioniert als Demo unter Chrome und IE, aber aus irgendeinem Grund nicht unter FF - siehe jsfiddle.net/C2YBE/31 . Ich fragte , warum FF hier nicht funktioniert: stackoverflow.com/questions/20747591/...
DVK

1
@DVK "Der Browser platziert alle" TODO "-Aufgaben (sowohl UI-Aufgaben als auch JavaScript-Befehle), die sich aus Ereignissen ergeben, in einer einzigen Warteschlange." Sir, können Sie bitte die Quelle dafür angeben? Imho arent Browser sollen unterschiedliche UI (Rendering Engine) und JS-Threads haben .... nichts für ungut beabsichtigt .... will nur lernen ..
bhavya_w

1
@bhavya_w Nein, alles passiert in einem Thread. Dies ist der Grund, warum eine lange js-Berechnung die Benutzeroberfläche blockieren kann
Seba Kerckhof

88

Schauen Sie sich John Resigs Artikel über die Funktionsweise von JavaScript-Timern an . Wenn Sie eine Zeitüberschreitung festlegen, wird der asynchrone Code tatsächlich in die Warteschlange gestellt, bis die Engine den aktuellen Aufrufstapel ausführt.


23

setTimeout() kauft Ihnen einige Zeit, bis die DOM-Elemente geladen sind, auch wenn sie auf 0 gesetzt sind.

Überprüfen Sie dies: setTimeout


23

Die meisten Browser haben einen Prozess namens Hauptthread, der für die Ausführung einiger JavaScript-Aufgaben, UI-Updates verantwortlich ist, z. B. Malen, Neuzeichnen oder Reflow usw.

Einige Aufgaben zur Ausführung von JavaScript und zur Aktualisierung der Benutzeroberfläche werden in die Warteschlange für Browsermeldungen gestellt und dann an den auszuführenden Hauptthread des Browsers gesendet.

Wenn UI-Updates generiert werden, während der Hauptthread beschäftigt ist, werden die Aufgaben zur Nachrichtenwarteschlange hinzugefügt.

setTimeout(fn, 0);Fügen Sie dies fnam Ende der auszuführenden Warteschlange hinzu. Es plant, dass eine Aufgabe nach einer bestimmten Zeit in die Nachrichtenwarteschlange aufgenommen wird.


"Alle JavaScript-Ausführungs- und UI-Aktualisierungsaufgaben werden dem Browser-Ereigniswarteschlangensystem hinzugefügt, und diese Aufgaben werden dann an den Haupt-UI-Thread des Browsers gesendet, um ausgeführt zu werden." .... Quelle bitte?
Bhavya_w

4
Hochleistungs-JavaScript (Nicholas Zakas, Stoyan Stefanov, Ross Harmes, Julien Lecomte und Matt Sweeney)
Arley

Stimmen Sie dafür ab add this fn to the end of the queue. Am wichtigsten ist, wo genau setTimeoutdiese Funktion, das Ende dieses Schleifenzyklus oder der Beginn des nächsten Schleifenzyklus hinzugefügt wird.
Grün

19

Hier gibt es widersprüchliche Antworten, und ohne Beweise gibt es keine Möglichkeit zu wissen, wem man glauben soll. Hier ist der Beweis, dass @DVK richtig und @SalvadorDali falsch ist. Letzterer behauptet:

"Und hier ist der Grund: Es ist nicht möglich, setTimeout mit einer Zeitverzögerung von 0 Millisekunden zu haben. Der Mindestwert wird vom Browser festgelegt und beträgt nicht 0 Millisekunden. Historisch gesehen setzen Browser dieses Minimum auf 10 Millisekunden, aber die HTML5-Spezifikationen und moderne Browser haben es auf 4 Millisekunden eingestellt. "

Die minimale Zeitüberschreitung von 4 ms ist für das, was passiert, irrelevant. Was wirklich passiert, ist, dass setTimeout die Rückruffunktion an das Ende der Ausführungswarteschlange schiebt. Wenn Sie nach setTimeout (Rückruf, 0) einen Sperrcode haben, dessen Ausführung einige Sekunden dauert, wird der Rückruf einige Sekunden lang nicht ausgeführt, bis der Sperrcode beendet ist. Versuchen Sie diesen 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

Ausgabe ist:

setting timeout 0 callback at 0
starting blocking loop at 5
blocking loop ended at 3000
in timeout callback at 3033

Ihre Antwort beweist nichts. Es zeigt nur, dass auf Ihrem Computer unter einer bestimmten Situation Computer Sie einige Zahlen werfen. Um etwas Verwandtes zu beweisen, benötigen Sie etwas mehr als ein paar Codezeilen und ein paar Zahlen.
Salvador Dali

6
@SalvadorDali, ich glaube, mein Beweis ist klar genug, damit die meisten Leute ihn verstehen. Ich denke, Sie fühlen sich defensiv und haben sich nicht die Mühe gemacht, es zu verstehen. Ich werde gerne versuchen, es zu klären, aber ich weiß nicht, was Sie nicht verstehen. Versuchen Sie, den Code auf Ihrem eigenen Computer auszuführen, wenn Sie meine Ergebnisse vermuten.
Vladimir Kornea

Ich werde versuchen, es zum letzten Mal klar zu machen. Meine Antwort klärt einige der interessanten Eigenschaften in Bezug auf Zeitüberschreitung und Intervall und wird von gültigen und erkennbaren Quellen gut unterstützt. Sie haben nachdrücklich behauptet, dass dies falsch ist, und auf Ihren Code verwiesen, den Sie aus irgendeinem Grund als Beweis bezeichnet haben. An Ihrem Code ist nichts auszusetzen (ich bin nicht dagegen). Ich sage nur, dass meine Antwort nicht falsch ist. Es werden einige Punkte klargestellt. Ich werde diesen Krieg beenden, weil ich keinen Punkt sehen kann.
Salvador Dali

15

Ein Grund dafür ist, die Ausführung von Code auf eine separate, nachfolgende Ereignisschleife zu verschieben. Wenn Sie auf ein Browserereignis reagieren (z. B. Mausklick), müssen Sie manchmal erst Operationen ausführen, nachdem das aktuelle Ereignis verarbeitet wurde. Die setTimeout()Einrichtung ist der einfachste Weg, dies zu tun.

Bearbeiten Sie jetzt, da es 2015 ist. Ich sollte beachten, dass es auch gibt requestAnimationFrame(), was nicht genau das gleiche ist, aber es ist nahe genug, setTimeout(fn, 0)dass es erwähnenswert ist.


Dies ist genau einer der Orte, an denen ich gesehen habe, wie es verwendet wird. =)
Zaibot


2
besteht darin, die Ausführung von Code auf eine separate nachfolgende Ereignisschleife zu verschieben : Wie ermitteln Sie eine nachfolgende Ereignisschleife ? Wie finden Sie heraus, was eine aktuelle Ereignisschleife ist? Woher weißt du, in welcher Event-Loop-Runde du jetzt bist?
Grün

@Green gut, das tust du nicht wirklich; Es gibt wirklich keinen direkten Einblick in die Funktionsweise der JavaScript-Laufzeit.
Pointy

requestAnimationFrame löste das Problem, dass IE und Firefox die Benutzeroberfläche manchmal nicht aktualisierten
David

9

Dies ist eine alte Frage mit alten Antworten. Ich wollte dieses Problem neu betrachten und beantworten, warum dies geschieht und nicht, warum dies nützlich ist.

Sie haben also zwei Funktionen:

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

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

und rufen Sie sie dann in der folgenden Reihenfolge f1(); f2();auf, um zu sehen, dass die zweite zuerst ausgeführt wird.

Und hier ist der Grund: Es ist nicht möglich, setTimeoutmit einer Zeitverzögerung von 0 Millisekunden zu haben. Der Mindestwert wird vom Browser festgelegt und beträgt nicht 0 Millisekunden. In der Vergangenheit haben Browser dieses Minimum auf 10 Millisekunden festgelegt, in den HTML5-Spezifikationen und modernen Browsern jedoch auf 4 Millisekunden.

Wenn die Verschachtelungsstufe größer als 5 und das Zeitlimit kleiner als 4 ist, erhöhen Sie das Zeitlimit auf 4.

Auch von Mozilla:

Um ein Zeitlimit von 0 ms in einem modernen Browser zu implementieren, können Sie window.postMessage () wie hier beschrieben verwenden .

PS-Informationen werden nach dem Lesen des folgenden Artikels entnommen .


1
@ user2407309 Machst du Witze? Sie meinen, dass die HTML5-Spezifikation falsch ist und Sie richtig sind? Lesen Sie die Quellen, bevor Sie abstimmen, und machen Sie starke Ansprüche geltend. Meine Antwort basiert auf der HTML-Spezifikation und der historischen Aufzeichnung. Anstatt die Antwort zu machen, die immer wieder das gleiche erklärt, habe ich etwas Neues hinzugefügt, etwas, das in früheren Antworten nicht gezeigt wurde. Ich sage nicht, dass dies der einzige Grund ist, ich zeige nur etwas Neues.
Salvador Dali

1
Dies ist falsch: "Und hier ist der Grund: Es ist nicht möglich, setTimeout mit einer Zeitverzögerung von 0 Millisekunden zu haben." Das ist nicht der Grund. Die Verzögerung von 4 ms spielt keine Rolle, warum dies setTimeout(fn,0)nützlich ist.
Vladimir Kornea

@ user2407309 es kann leicht geändert werden zu "um die von anderen angegebenen Gründe hinzuzufügen, ist es nicht möglich ....". Es ist also lächerlich, nur deswegen abzustimmen, besonders wenn Ihre eigene Antwort nichts Neues sagt. Nur eine kleine Bearbeitung würde ausreichen.
Salvador Dali

10
Salvador Dali: Wenn Sie die emotionalen Aspekte des Mikroflammenkrieges hier ignorieren, müssen Sie wahrscheinlich zugeben, dass @VladimirKornea Recht hat. Es ist wahr, dass Browser eine Verzögerung von 0 ms auf 4 ms zuordnen, aber selbst wenn dies nicht der Fall wäre, wären die Ergebnisse immer noch dieselben. Der treibende Mechanismus hierbei ist, dass Code in die Warteschlange und nicht in den Aufrufstapel verschoben wird. Werfen Sie einen Blick auf diese hervorragende JSConf-Präsentation, es kann hilfreich sein, das Problem zu klären: youtube.com/watch?v=8aGhZQkoFbQ
Hashchange

1
Ich bin verwirrt darüber, warum Sie glauben, dass Ihr Angebot für ein qualifiziertes Minimum von 4 ms ein globales Minimum von 4 ms ist. Wie Ihr Zitat aus der HTML5-Spezifikation zeigt, beträgt das Minimum nur 4 ms, wenn Sie Anrufe in setTimeout/ setIntervalmehr als fünf Ebenen tief verschachtelt haben . Wenn Sie dies nicht getan haben, beträgt das Minimum 0 ms (was bei fehlenden Zeitmaschinen der Fall ist). Mozillas Dokumente erweitern dies, um wiederholte, nicht nur verschachtelte Fälle abzudecken ( setIntervalmit einem Intervall von 0 wird dies sofort einige Male neu geplant und danach länger verzögert), aber einfache Verwendungen setTimeoutmit minimaler Verschachtelung können sofort in die Warteschlange gestellt werden.
ShadowRanger

8

Da es eine Dauer von übergeben wird 0, wird es vermutlich verwendet, um den an den übergebenen Code setTimeoutaus dem Ausführungsfluss zu entfernen . Wenn es sich also um eine Funktion handelt, die eine Weile dauern kann, wird die Ausführung des nachfolgenden Codes nicht verhindert.



7

Diese beiden am besten bewerteten Antworten sind falsch. Schauen Sie sich die MDN-Beschreibung des Parallelitätsmodells und der Ereignisschleife an , und es sollte klar werden, was los ist (diese MDN-Ressource ist ein echtes Juwel). Und einfach zu verwenden setTimeout kann unerwartete Probleme in Ihren Code einfügen und dieses kleine Problem "lösen".

Was hier tatsächlich vor sich geht, ist nicht, dass "der Browser möglicherweise noch nicht ganz bereit ist, weil Parallelität" oder etwas, das auf "jede Zeile ist ein Ereignis, das am Ende der Warteschlange hinzugefügt wird" basiert.

Die von DVK bereitgestellte jsfiddle zeigt zwar ein Problem, aber seine Erklärung dafür ist nicht korrekt.

Was in seinem Code passiert, ist, dass er zuerst einen Ereignishandler an das clickEreignis auf der #doSchaltfläche anfügt.

Wenn Sie dann tatsächlich auf die Schaltfläche klicken, messagewird a erstellt, das auf die Ereignishandlerfunktion verweist, die der hinzugefügt wird message queue. Wenn der event loopdiese Nachricht erreicht, wird frameauf dem Stapel ein Funktionsaufruf für den Click-Event-Handler in der jsfiddle erstellt.

Und hier wird es interessant. Wir sind es so gewohnt, Javascript als asynchron zu betrachten, dass wir diese winzige Tatsache übersehen können: Jeder Frame muss vollständig ausgeführt werden, bevor der nächste Frame ausgeführt werden kann . Keine Parallelität, Leute.

Was bedeutet das? Dies bedeutet, dass bei jedem Aufruf einer Funktion aus der Nachrichtenwarteschlange die Warteschlange blockiert wird, bis der von ihr generierte Stapel geleert wurde. Oder allgemeiner gesagt, es wird blockiert, bis die Funktion zurückgekehrt ist. Und es blockiert alles , einschließlich DOM-Rendering-Operationen, Scrollen und so weiter. Wenn Sie eine Bestätigung wünschen, versuchen Sie einfach, die Dauer des lang laufenden Vorgangs in der Geige zu verlängern (z. B. die äußere Schleife noch 10 Mal ausführen), und Sie werden feststellen, dass Sie während der Ausführung nicht durch die Seite scrollen können. Wenn es lange genug läuft, werden Sie von Ihrem Browser gefragt, ob Sie den Vorgang abbrechen möchten, da die Seite dadurch nicht mehr reagiert. Der Frame wird ausgeführt, und die Ereignisschleife und die Nachrichtenwarteschlange bleiben hängen, bis sie beendet sind.

Warum wird dieser Nebeneffekt des Textes nicht aktualisiert? Denn während Sie haben den Wert des Elements in der DOM geändert - man kann console.log()seinen Wert unmittelbar nach dem Wechsel und sehen , dass es hat sich geändert (die zeigt , warum DVK Erklärung nicht korrekt ist) - der Browser für den Stapel zu verarmen wartet (die onHandlerfunktion, die zurückgegeben werden soll) und damit die Nachricht, die beendet werden soll, damit sie schließlich die Nachricht ausführen kann, die von der Laufzeit als Reaktion auf unsere Mutationsoperation hinzugefügt wurde, und um diese Mutation in der Benutzeroberfläche wiederzugeben .

Dies liegt daran, dass wir tatsächlich darauf warten, dass der Code ausgeführt wird. Wir haben nicht gesagt, dass "jemand dies abruft und diese Funktion dann mit den Ergebnissen aufruft, danke, und jetzt bin ich fertig, also kehren Sie zurück, machen Sie jetzt was auch immer", wie wir es normalerweise mit unserem ereignisbasierten asynchronen Javascript tun. Wir geben eine Funktion Ereignishandler klicken, wir ein DOM - Element aktualisieren, rufen wir eine weitere Funktion, die andere Funktion arbeitet für eine lange Zeit und dann zurückkehrt, wir dann das gleiche DOM - Element aktualisieren, und dann kehren wir von der anfänglichen Funktion, effektiv Entleerung der Stapel. Und dann kann der Browser zur nächsten Nachricht in der Warteschlange gelangen, bei der es sich möglicherweise um eine Nachricht handelt, die von uns durch Auslösen eines internen Ereignisses vom Typ "On-DOM-Mutation" generiert wurde.

Die Benutzeroberfläche des Browsers kann die Benutzeroberfläche nicht aktualisieren (oder möchte dies nicht tun), bis der aktuell ausgeführte Frame abgeschlossen ist (die Funktion ist zurückgekehrt). Persönlich denke ich, dass dies eher beabsichtigt als einschränkend ist.

Warum funktioniert das setTimeoutDing dann? Dies geschieht, weil der Aufruf der Funktion mit langer Laufzeit effektiv aus dem eigenen Frame entfernt wird und die Ausführung später im windowKontext geplant wird, sodass er selbst sofort zurückkehren und der Nachrichtenwarteschlange die Verarbeitung anderer Nachrichten ermöglichen kann. Die Idee ist, dass die Nachricht "On Update" der Benutzeroberfläche, die von uns in Javascript beim Ändern des Texts im DOM ausgelöst wurde, jetzt vor der Nachricht steht, die für die lang laufende Funktion in die Warteschlange gestellt wurde, sodass die Aktualisierung der Benutzeroberfläche vor dem Blockieren erfolgt Für eine lange Zeit.

Beachten Sie, dass a) die Langzeitfunktion bei der Ausführung immer noch alles blockiert und b) Ihnen nicht garantiert werden kann, dass das UI-Update in der Nachrichtenwarteschlange tatsächlich vor Ihnen liegt. In meinem Chrome-Browser vom Juni 2018 0"behebt" ein Wert von nicht das Problem, das die Geige demonstriert - 10. Ich bin tatsächlich ein bisschen erstickt, weil es mir logisch erscheint, dass die UI-Aktualisierungsnachricht davor in die Warteschlange gestellt werden sollte, da ihr Trigger ausgeführt wird, bevor die Langzeitfunktion "später" ausgeführt wird. Aber vielleicht gibt es einige Optimierungen in der V8-Engine, die stören könnten, oder vielleicht fehlt mir einfach mein Verständnis.

Okay, was ist das Problem bei der Verwendung setTimeoutund was ist eine bessere Lösung für diesen speziellen Fall?

Zunächst einmal ist das Problem bei der Verwendung setTimeouteines solchen Ereignishandlers, um ein anderes Problem zu beheben, anfällig für Probleme mit anderem Code. Hier ist ein reales Beispiel aus meiner Arbeit:

Ein Kollege versuchte in einem falsch informierten Verständnis der Ereignisschleife, Javascript zu "fädeln", indem er einen Code setTimeout 0zum Rendern von Vorlagen für das Rendern verwendete. Er ist nicht mehr hier, um zu fragen, aber ich kann davon ausgehen, dass er möglicherweise Timer eingefügt hat, um die Rendergeschwindigkeit zu messen (was die sofortige Rückgabe von Funktionen bedeuten würde), und festgestellt hat, dass die Verwendung dieses Ansatzes zu unglaublich schnellen Antworten dieser Funktion führen würde.

Das erste Problem ist offensichtlich; Sie können kein Javascript einfädeln, daher gewinnen Sie hier nichts, während Sie die Verschleierung hinzufügen. Zweitens haben Sie jetzt das Rendern einer Vorlage effektiv vom Stapel möglicher Ereignis-Listener getrennt, die möglicherweise erwarten, dass genau diese Vorlage gerendert wurde, obwohl dies möglicherweise nicht der Fall war. Das tatsächliche Verhalten dieser Funktion war jetzt nicht deterministisch, ebenso wie - unwissentlich - jede Funktion, die sie ausführen würde oder von ihr abhängen würde. Sie können fundierte Vermutungen anstellen, aber Sie können das Verhalten nicht richtig codieren.

Das "Update" beim Schreiben eines neuen Ereignishandlers, der von seiner Logik abhing, sollte ebenfalls verwendet werden setTimeout 0. Aber das ist keine Lösung, es ist schwer zu verstehen und es macht keinen Spaß, Fehler zu debuggen, die durch solchen Code verursacht werden. Manchmal gibt es nie ein Problem, manchmal schlägt es immer wieder fehl, und manchmal funktioniert es und bricht sporadisch ab, abhängig von der aktuellen Leistung der Plattform und dem, was gerade passiert. Aus diesem Grund würde ich persönlich davon abraten, diesen Hack (es) zu verwenden ist ein Hack, und wir sollten alle wissen, dass dies der Fall ist), es sei denn, Sie wissen wirklich, was Sie tun und welche Konsequenzen dies hat.

Aber was kann wir stattdessen tun? Wie aus dem MDN-Artikel hervorgeht, teilen Sie die Arbeit entweder in mehrere Nachrichten auf (wenn Sie können), damit andere Nachrichten, die sich in der Warteschlange befinden, mit Ihrer Arbeit verschachtelt und ausgeführt werden, während sie ausgeführt wird, oder verwenden Sie einen Web-Worker, der ausgeführt werden kann in Verbindung mit Ihrer Seite und geben Ergebnisse zurück, wenn die Berechnungen abgeschlossen sind.

Oh, und wenn Sie denken: "Nun, könnte ich nicht einfach einen Rückruf in die Langzeitfunktion einfügen, um sie asynchron zu machen?", Dann nein. Der Rückruf macht es nicht asynchron, es muss immer noch der lang laufende Code ausgeführt werden, bevor Ihr Rückruf explizit aufgerufen wird.


3

Das andere, was dies tut, ist, den Funktionsaufruf an den unteren Rand des Stapels zu verschieben, um einen Stapelüberlauf zu verhindern, wenn Sie eine Funktion rekursiv aufrufen. Dies hat den Effekt einer whileSchleife, lässt jedoch die JavaScript-Engine andere asynchrone Timer auslösen.


Stimmen Sie dafür ab push the function invocation to the bottom of the stack. Was stackdu redest, ist dunkel. Am wichtigsten ist, wo genau setTimeoutdiese Funktion, das Ende dieses Schleifenzyklus oder der Beginn des nächsten Schleifenzyklus hinzugefügt wird.
Grün

1

Durch Aufrufen von setTimeout geben Sie der Seite Zeit, auf die Aktionen des Benutzers zu reagieren. Dies ist besonders hilfreich für Funktionen, die während des Ladens der Seite ausgeführt werden.


1

Einige andere Fälle, in denen setTimeout nützlich ist:

Sie möchten eine lange laufende Schleife oder Berechnung in kleinere Komponenten aufteilen, damit der Browser nicht "einfriert" oder "Skript auf Seite ist beschäftigt" sagt.

Sie möchten eine Schaltfläche zum Senden von Formularen deaktivieren, wenn Sie darauf klicken. Wenn Sie jedoch die Schaltfläche im onClick-Handler deaktivieren, wird das Formular nicht gesendet. setTimeout mit einer Zeit von Null macht den Trick, indem das Ereignis beendet wird, das Formular mit dem Senden beginnt und Ihre Schaltfläche deaktiviert werden kann.


2
Das Deaktivieren ist im Onsubmit-Ereignis besser. Es wäre schneller und wird garantiert aufgerufen, bevor das Formular technisch gesendet wird, da Sie die Übermittlung stoppen können.
Kris

Sehr richtig. Ich nehme an, das Deaktivieren von Onclicks ist für das Prototyping einfacher, da Sie einfach onclick = "this.disabled = true" in die Schaltfläche eingeben können, während das Deaktivieren beim Senden etwas mehr Arbeit erfordert.
Fabspro

1

Die Antworten zu Ausführungsschleifen und zum Rendern des DOM, bevor ein anderer Code abgeschlossen ist, sind korrekt. Null-Sekunden-Timeouts in JavaScript helfen dabei, den Code pseudo-multithreaded zu machen, obwohl dies nicht der Fall ist.

Ich möchte hinzufügen, dass der BESTE Wert für ein browser- / plattformübergreifendes Null-Sekunden-Timeout in JavaScript tatsächlich etwa 20 Millisekunden anstelle von 0 (Null) beträgt, da viele mobile Browser aufgrund von Taktbeschränkungen keine Timeouts von weniger als 20 Millisekunden registrieren können auf AMD-Chips.

Außerdem sollten lang laufende Prozesse, die keine DOM-Manipulation beinhalten, jetzt an Web Worker gesendet werden, da sie eine echte Multithread-Ausführung von JavaScript ermöglichen.


2
Ich bin etwas skeptisch gegenüber Ihrer Antwort, habe sie jedoch positiv bewertet, da ich gezwungen war, zusätzliche Nachforschungen über Browserstandards anzustellen. Bei der Recherche nach Standards gehe ich dorthin, wo ich immer bin. MDN: developer.mozilla.org/en-US/docs/Web/API/window.setTimeout In der HTML5-Spezifikation heißt es 4 ms. Es sagt nichts über Taktbeschränkungen auf mobilen Chips aus. Es fällt Ihnen schwer, nach einer Informationsquelle zu suchen, um Ihre Aussagen zu stützen. Habe herausgefunden, dass Dart Language von Google setTimeout insgesamt zugunsten eines Timer-Objekts entfernt hat.
John Zabroski

8
(...) weil viele mobile Browser aufgrund von Taktbeschränkungen keine Zeitüberschreitungen von weniger als 20 Millisekunden registrieren können (...) Jede Plattform hat aufgrund ihrer Uhr Zeitbeschränkungen und keine Plattform ist in der Lage, das nächste Ding genau 0 ms nach dem auszuführen der Momentane. Bei einem Timeout von 0 ms wird die Ausführung einer Funktion so schnell wie möglich angefordert, und zeitliche Einschränkungen einer bestimmten Plattform ändern in keiner Weise die Bedeutung dieser Funktion.
Piotr Dobrogost

1

setTimout on 0 ist auch sehr nützlich beim Einrichten eines aufgeschobenen Versprechens, das Sie sofort zurückgeben möchten:

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

Das Problem war, dass Sie versucht haben, eine Javascript-Operation für ein nicht vorhandenes Element auszuführen. Das Element musste noch geladen werden und setTimeout()bietet mehr Zeit zum Laden eines Elements auf folgende Weise:

  1. setTimeout()bewirkt, dass das Ereignis ansynchron ist und daher nach dem gesamten synchronen Code ausgeführt wird, sodass Ihr Element mehr Zeit zum Laden hat. Asynchrone Rückrufe wie der Rückruf in setTimeout()werden in die Ereigniswarteschlange gestellt und von der Ereignisschleife auf den Stapel gelegt nachdem der Stapel des synchronen Codes leer ist.
  2. Der Wert 0 für ms als zweites Argument in der Funktion setTimeout()ist häufig etwas höher (4-10 ms je nach Browser). Diese etwas höhere Zeit, die zum Ausführen der setTimeout()Rückrufe benötigt wird, wird durch die Anzahl der "Ticks" (die ein Tick einen Rückruf auf den Stapel drückt, wenn der Stapel leer ist) der Ereignisschleife verursacht. Aus Gründen der Leistung und der Akkulaufzeit ist die Anzahl der Ticks in der Ereignisschleife auf eine bestimmte Anzahl von weniger als 1000 Mal pro Sekunde beschränkt.

-1

Javascript ist eine Single-Threaded-Anwendung, die es nicht erlaubt, Funktionen gleichzeitig auszuführen, um dieses Ereignis zu erreichen. Es werden Schleifen verwendet. Was genau setTimeout (fn, 0) tut, ist, dass es sich um eine Aufgabenquest handelt, die ausgeführt wird, wenn Ihr Aufrufstapel leer ist. Ich weiß, dass diese Erklärung ziemlich langweilig ist, daher empfehle ich Ihnen, dieses Video durchzugehen. Dies wird Ihnen helfen, wie die Dinge unter der Haube im Browser funktionieren. Schauen Sie sich dieses Video an: - https://www.youtube.com/watch?time_continue=392&v=8aGhZQkoFbQ

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.