Dlaczego czasem setTimeout (fn, 0) jest czasem użyteczny?


872

Niedawno napotkałem dość paskudny błąd, w którym kod ładował się <select>dynamicznie przez JavaScript. Ta dynamicznie ładowana <select>miała wstępnie wybraną wartość. W IE6, mieliśmy już kod naprawić wybrany <option>, ponieważ czasami <select>jest selectedIndexwartość byłby zsynchronizowany z wybranym <option>'s indexatrybut, jak poniżej:

field.selectedIndex = element.index;

Jednak ten kod nie działał. Nawet jeśli pole selectedIndexzostało ustawione poprawnie, niewłaściwy indeks ostatecznie zostałby wybrany. Jeśli jednak wstawię alert()instrukcję w odpowiednim czasie, wybrana zostanie właściwa opcja. Myśląc, że to może być jakiś problem z czasem, wypróbowałem coś losowego, co widziałem wcześniej w kodzie:

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

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

I to zadziałało!

Mam rozwiązanie mojego problemu, ale nie jestem pewien, czy nie wiem dokładnie, dlaczego to rozwiązuje mój problem. Czy ktoś ma oficjalne wyjaśnienie? Jakiego problemu z przeglądarką unikam, nazywając moją funkcję „później” przy użyciu setTimeout()?


2
Większość pytań opisuje, dlaczego jest to przydatne. Jeśli chcesz wiedzieć, dlaczego tak się dzieje - przeczytaj moją odpowiedź: stackoverflow.com/a/23747597/1090562
Salvador Dali

18
Philip Roberts wyjaśnia to w najlepszy możliwy sposób tutaj w swoim przemówieniu „Co do cholery jest pętlą zdarzeń?” youtube.com/watch?v=8aGhZQkoFbQ
vasa

Jeśli się spieszysz, jest to część filmu, w której zaczyna dokładnie odpowiadać na pytanie: youtu.be/8aGhZQkoFbQ?t=14m54s . Niezależnie od tego cały film z pewnością jest wart obejrzenia.
William Hampshire,

4
setTimeout(fn)jest taki sam jak setTimeout(fn, 0), btw.
vsync,

Odpowiedzi:


827

W pytaniu istniał warunek wyścigu między:

  1. Próba zainicjowania listy rozwijanej przez przeglądarkę, gotowa do aktualizacji wybranego indeksu oraz
  2. Twój kod, aby ustawić wybrany indeks

Twój kod konsekwentnie wygrywał ten wyścig i próbował ustawić rozwijany wybór, zanim przeglądarka była gotowa, co oznacza, że ​​pojawi się błąd.

Ta rasa istniała, ponieważ JavaScript ma jeden wątek wykonania, który jest współdzielony z renderowaniem strony. W efekcie uruchomienie JavaScript blokuje aktualizację DOM.

Twoje obejście polegało na:

setTimeout(callback, 0)

Wywołanie setTimeoutz wywołaniem zwrotnym, a zero jako drugi argument, zaplanuje, aby wywołanie zwrotne było uruchamiane asynchronicznie , po jak najkrótszym możliwym opóźnieniu - co będzie około 10 ms, gdy karta jest aktywna, a wątek wykonywania JavaScript nie jest zajęty.

Rozwiązaniem OP było zatem opóźnienie o około 10 ms, ustawienie wybranego indeksu. To dało przeglądarce możliwość zainicjowania DOM, naprawiając błąd.

Każda wersja programu Internet Explorer wykazywała dziwaczne zachowania, a tego rodzaju obejście było czasami konieczne. Ewentualnie mógł to być prawdziwy błąd w bazie kodu PO.


Zobacz przemówienie Philipa Robertsa „Co do cholery jest pętlą zdarzeń?” po dokładniejsze wyjaśnienia.


276
„Rozwiązaniem jest„ wstrzymanie ”wykonania JavaScript, aby wątki renderujące mogły nadrobić zaległości.” Nie do końca prawda, setTimeout dodaje nowe zdarzenie do kolejki zdarzeń przeglądarki, a silnik renderowania znajduje się już w tej kolejce (nie do końca prawda, ale wystarczająco blisko), więc jest wykonywany przed zdarzeniem setTimeout.
David Mulder

48
Tak, to jest o wiele bardziej szczegółowa i poprawna odpowiedź. Ale moja jest „wystarczająco poprawna”, aby ludzie zrozumieli, dlaczego ta sztuczka działa.
staticsan

2
@DavidMulder, czy oznacza to, że przeglądarka parsuje css i renderuje w innym wątku niż wątek wykonawczy javascript?
jason

8
Nie, są one parsowane w zasadzie w tym samym wątku, w przeciwnym razie kilka linii manipulacji DOM spowodowałoby cały czas przepływy, co miałoby bardzo zły wpływ na szybkość wykonania.
David Mulder

30
Ten film jest najlepszym wyjaśnieniem, dlaczego my setTimeout 0 2014.jsconf.eu/speakers/…
davibq

665

Przedmowa:

Niektóre inne odpowiedzi są poprawne, ale w rzeczywistości nie ilustrują rozwiązania problemu, dlatego stworzyłem tę odpowiedź, aby przedstawić tę szczegółową ilustrację.

W związku z tym zamieszczam szczegółowy przegląd tego, co robi przeglądarka i jak setTimeout()pomaga korzystanie z niej . Wygląda na długotrwały, ale w rzeczywistości jest bardzo prosty i bezpośredni - właśnie uczyniłem go bardzo szczegółowym.

AKTUALIZACJA: Zrobiłem JSFiddle, aby zademonstrować na żywo wyjaśnienie poniżej: http://jsfiddle.net/C2YBE/31/ . Wiele dzięki do @ThangChung za pomoc, aby go kickstart.

AKTUALIZACJA2: Na wypadek śmierci strony JSFiddle lub usunięcia kodu, dodałem kod do tej odpowiedzi na samym końcu.


SZCZEGÓŁY :

Wyobraź sobie aplikację internetową z przyciskiem „zrób coś” i div div.

Moduł onClickobsługi przycisku „zrób coś” wywołuje funkcję „LongCalc ()”, która wykonuje 2 czynności:

  1. Wykonuje bardzo długie obliczenia (powiedzmy, że zajmuje 3 minuty)

  2. Drukuje wyniki obliczeń w div wyniku.

Teraz użytkownicy zaczynają testować to, klikają przycisk „zrób coś”, a strona siedzi tam, robiąc pozornie nic przez 3 minuty, stają się niespokojni, klikają przycisk ponownie, czekają 1 minutę, nic się nie dzieje, ponownie przyciskają ...

Problem jest oczywisty - chcesz DIV „Status”, który pokazuje, co się dzieje. Zobaczmy, jak to działa.


Więc dodajesz DIV „Status” (początkowo pusty) i modyfikujesz onclickmoduł obsługi (funkcję LongCalc()), aby zrobić 4 rzeczy:

  1. Wypełnij status „Obliczanie ... może zająć ~ 3 minuty” do statusu DIV

  2. Wykonuje bardzo długie obliczenia (powiedzmy, że zajmuje 3 minuty)

  3. Drukuje wyniki obliczeń w div wyniku.

  4. Wpisz status „Obliczenie zakończone” w status DIV

Z przyjemnością dajesz aplikację użytkownikom do ponownego przetestowania.

Wracają do ciebie wyglądając na bardzo wściekłych. I wyjaśnij, że kiedy kliknęli przycisk, status DIV nigdy nie był aktualizowany o status „Obliczanie ...” !!!


Drapiesz się po głowie, pytasz na StackOverflow (lub czytasz dokumenty lub google) i zdajesz sobie sprawę z problemu:

Przeglądarka umieszcza wszystkie zadania „DO ZROBIENIA” (zarówno zadania interfejsu użytkownika, jak i polecenia JavaScript) wynikające ze zdarzeń w jednej kolejce . I niestety ponowne narysowanie DIV „Status” z nową wartością „Obliczanie ...” to osobne TODO, które trafia na koniec kolejki!

Oto rozkład zdarzeń podczas testu użytkownika, zawartość kolejki po każdym zdarzeniu:

  • Kolejka: [Empty]
  • Wydarzenie: Kliknij przycisk. Kolejka po wydarzeniu:[Execute OnClick handler(lines 1-4)]
  • Zdarzenie: Wykonaj pierwszy wiersz w module obsługi OnClick (np. Zmień wartość Status DIV). W kolejce po wypadku: [Execute OnClick handler(lines 2-4), re-draw Status DIV with new "Calculating" value]. Należy pamiętać, że podczas gdy zmiany DOM następują natychmiast, aby ponownie narysować odpowiedni element DOM, potrzebne jest nowe zdarzenie, wywołane zmianą DOM, które poszło na końcu kolejki .
  • PROBLEM!!! PROBLEM!!! Szczegóły wyjaśnione poniżej.
  • Zdarzenie: Wykonaj drugą linię w module obsługi (obliczenia). W kolejce po: [Execute OnClick handler(lines 3-4), re-draw Status DIV with "Calculating" value].
  • Zdarzenie: Wykonaj 3. linię w module obsługi (wypełnij wynik DIV). W kolejce po: [Execute OnClick handler(line 4), re-draw Status DIV with "Calculating" value, re-draw result DIV with result].
  • Zdarzenie: Wykonaj 4. linię w module obsługi (wypełnij status DIV „DONE”). Kolejka: [Execute OnClick handler, re-draw Status DIV with "Calculating" value, re-draw result DIV with result; re-draw Status DIV with "DONE" value].
  • Zdarzenie: wykonanie sugerowane returnz onclicksub-programu obsługi. Usuwamy „Execute OnClick handler” z kolejki i rozpoczynamy wykonywanie następnego elementu w kolejce.
  • UWAGA: Ponieważ już zakończyliśmy obliczenia, dla użytkownika minęły już 3 minuty. Ponowne losowanie jeszcze się nie wydarzyło !!!
  • Zdarzenie: ponownie losuj Status DIV z wartością „Obliczanie”. Ponownie losujemy i usuwamy z kolejki.
  • Zdarzenie: ponownie losuj Wynik DIV z wartością wyniku. Ponownie losujemy i usuwamy z kolejki.
  • Wydarzenie: ponownie losuj Status DIV z wartością „Gotowe”. Ponownie losujemy i usuwamy z kolejki. Widzowie o ostrych oczach mogą nawet zauważyć „Status DIV z miganiem wartości„ Obliczanie ”przez ułamek mikrosekundy - PO ZAKOŃCZENIU OBLICZEŃ

Tak więc podstawowym problemem jest to, że zdarzenie ponownego losowania DIV „Status” jest umieszczane w kolejce na końcu, PO zdarzeniu „wykonaj linię 2”, które zajmuje 3 minuty, więc faktyczne ponowne losowanie nie następuje, dopóki PO zakończeniu obliczeń.


Na ratunek przychodzi setTimeout(). Jak to pomaga? Ponieważ poprzez wywoływanie długo działającego kodu setTimeout, faktycznie tworzysz 2 zdarzenia: setTimeoutwykonanie i (z powodu przekroczenia limitu czasu 0) osobny wpis kolejki dla wykonywanego kodu.

Tak więc, aby rozwiązać problem, modyfikujesz swój onClickmoduł obsługi, aby był DWIEMI instrukcjami (w nowej funkcji lub tylko w bloku onClick):

  1. Wypełnij status „Obliczanie ... może zająć ~ 3 minuty” do statusu DIV

  2. Wykonaj setTimeout()z limitem czasu 0 i wywołaniem LongCalc()funkcji .

    LongCalc()funkcja jest prawie taka sama jak poprzednio, ale oczywiście nie ma aktualizacji DIV statusu „Obliczanie ...” jako pierwszego kroku; i zamiast tego natychmiast rozpoczyna obliczenia.

Jak teraz wygląda sekwencja zdarzeń i kolejka?

  • Kolejka: [Empty]
  • Wydarzenie: Kliknij przycisk. Kolejka po wydarzeniu:[Execute OnClick handler(status update, setTimeout() call)]
  • Zdarzenie: Wykonaj pierwszy wiersz w module obsługi OnClick (np. Zmień wartość Status DIV). W kolejce po wypadku: [Execute OnClick handler(which is a setTimeout call), re-draw Status DIV with new "Calculating" value].
  • Zdarzenie: Wykonaj drugą linię w module obsługi (wywołanie setTimeout). W kolejce po: [re-draw Status DIV with "Calculating" value]. W kolejce nie ma nic nowego przez kolejne 0 sekund.
  • Zdarzenie: Alarm przekroczenia limitu czasu wyłączy się, 0 sekund później. W kolejce po: [re-draw Status DIV with "Calculating" value, execute LongCalc (lines 1-3)].
  • Zdarzenie: ponownie losuj Status DIV z wartością „Obliczanie” . W kolejce po: [execute LongCalc (lines 1-3)]. Pamiętaj, że to ponowne losowanie może nastąpić PRZED włączeniem alarmu, który działa równie dobrze.
  • ...

Brawo! Status DIV właśnie został zaktualizowany do „Obliczania ...” przed rozpoczęciem obliczeń !!!



Poniżej znajduje się przykładowy kod z JSFiddle ilustrujący te przykłady: http://jsfiddle.net/C2YBE/31/ :

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

Kod JavaScript: (Wykonany onDomReadyi może wymagać 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
świetna odpowiedź DVK! Oto lista
kumikoda

4
Naprawdę fajna odpowiedź, DVK. Aby ułatwić sobie wyobrażenie, umieściłem ten kod na jsfiddle jsfiddle.net/thangchung/LVAaV
thangchung

4
@ThangChung - Próbowałem stworzyć lepszą wersję (2 przyciski, po jednym dla każdego przypadku) w JSFiddle. Działa jako wersja demonstracyjna w Chrome i IE, ale z jakiegoś powodu nie działa w FF - patrz jsfiddle.net/C2YBE/31 . Zapytałem, dlaczego FF tutaj nie działa: stackoverflow.com/questions/20747591/...
DVK

1
@DVK „Przeglądarka umieszcza wszystkie zadania„ DO ZROBIENIA ”(zarówno zadania interfejsu użytkownika, jak i polecenia JavaScript) wynikające ze zdarzeń w jednej kolejce”. Proszę pana, proszę podać źródło tego? Nie są to przeglądarki, które mają mieć różne interfejsy użytkownika (silnik renderujący) i wątki JS .... bez zamierzonego przestępstwa ... po prostu chcę się nauczyć ...
bhavya_w

1
@bhavya_w Nie, wszystko dzieje się w jednym wątku. To jest powód, dla którego długie obliczenia js mogą blokować interfejs użytkownika
Seba Kerckhof

88

Przeczytaj artykuł Johna Resiga na temat działania timerów JavaScript . Ustawienie limitu czasu powoduje, że kod asynchroniczny jest w kolejce, dopóki silnik nie wykona bieżącego stosu wywołań.



23

Większość przeglądarek ma proces zwany głównym wątkiem, który jest odpowiedzialny za wykonywanie niektórych zadań JavaScript, aktualizacji interfejsu użytkownika, np .: malowanie, przerysowanie lub ponowne wlanie itp.

Niektóre zadania JavaScript i aktualizacji interfejsu użytkownika są umieszczane w kolejce w kolejce komunikatów przeglądarki, a następnie wysyłane do głównego wątku przeglądarki, który ma zostać wykonany.

Gdy aktualizacje interfejsu użytkownika są generowane, gdy główny wątek jest zajęty, zadania są dodawane do kolejki komunikatów.

setTimeout(fn, 0);dodaj to fnna końcu kolejki do wykonania. Planuje dodanie zadania do kolejki komunikatów po upływie określonego czasu.


„Każde wykonanie JavaScript i zadania aktualizacji interfejsu użytkownika są dodawane do systemu kolejek zdarzeń przeglądarki, a następnie zadania te są wysyłane do głównego wątku interfejsu użytkownika przeglądarki, który ma zostać wykonany.… Źródło proszę?
bhavya_w

4
JavaScript o wysokiej wydajności (Nicholas Zakas, Stoyan Stefanov, Ross Harmes, Julien Lecomte i Matt Sweeney)
Arley

Głosuj za to add this fn to the end of the queue. Najważniejsze jest to, gdzie dokładnie setTimeoutdodaje tę funkcję, koniec tego cyklu pętli lub sam początek następnego cyklu pętli.
Zielony

19

Są tu sprzeczne uprzywilejowane odpowiedzi i bez dowodów nie ma sposobu, aby wiedzieć, w kogo uwierzyć. Oto dowód, że @DVK ma rację, a @SalvadorDali jest niepoprawny. Ten ostatni twierdzi:

„I oto dlaczego: nie ma możliwości ustawienia setTimeout z opóźnieniem 0 milisekund. Minimalna wartość jest określana przez przeglądarkę i nie wynosi 0 milisekund. Historycznie przeglądarki ustawiają to minimum na 10 milisekund, ale specyfikacje HTML5 i nowoczesne przeglądarki ustawiają go na 4 milisekundy ”.

Limit czasu wynoszący 4 ms nie ma znaczenia dla tego, co się dzieje. Tak naprawdę dzieje się tak, że setTimeout wypycha funkcję zwrotną na koniec kolejki wykonawczej. Jeśli po setTimeout (wywołanie zwrotne, 0) masz kod blokujący, którego uruchomienie zajmuje kilka sekund, wywołanie zwrotne nie zostanie wykonane przez kilka sekund, dopóki kod blokujący nie zakończy się. Wypróbuj ten kod:

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

Dane wyjściowe to:

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

twoja odpowiedź nic nie dowodzi. To po prostu pokazuje, że na twoim komputerze w określonej sytuacji komputer rzuca ci kilka liczb. Aby udowodnić coś związanego, potrzebujesz trochę więcej niż kilku linii kodu i kilku liczb.
Salvador Dali,

6
@SalvadorDali, uważam, że mój dowód jest wystarczająco jasny, aby większość ludzi mogła go zrozumieć. Myślę, że czujesz się defensywny i nie starałeś się tego zrozumieć. Z przyjemnością spróbuję to wyjaśnić, ale nie wiem, czego nie rozumiesz. Jeśli podejrzewasz moje wyniki, spróbuj uruchomić kod na własnym komputerze.
Vladimir Kornea

Spróbuję to wyjaśnić po raz ostatni. Moja odpowiedź wyjaśnia niektóre interesujące właściwości dotyczące limitu czasu i interwału oraz jest dobrze poparta ważnymi i rozpoznawalnymi źródłami. Mocno twierdziłeś, że jest to błąd i wskazałeś swój kod, który z jakiegoś powodu nazwałeś dowodem. Nie ma nic złego w twoim kodzie (nie jestem temu przeciwny). Mówię tylko, że moja odpowiedź nie jest zła. Wyjaśnia niektóre punkty. Zamierzam zatrzymać tę wojnę, ponieważ nie widzę sensu.
Salvador Dali

15

Jednym z powodów tego jest odroczenie wykonania kodu w osobnej, kolejnej pętli zdarzeń. Reagując na jakieś zdarzenie w przeglądarce (na przykład kliknięcie myszą), czasami konieczne jest wykonanie operacji dopiero po przetworzeniu bieżącego zdarzenia. setTimeout()Zakład jest najprostszym sposobem, aby to zrobić.

edytuj teraz, gdy jest 2015, powinienem zauważyć, że jest też coś requestAnimationFrame(), co nie jest dokładnie takie samo, ale jest wystarczająco blisko setTimeout(fn, 0), że warto o nim wspomnieć.


To jest dokładnie jedno z miejsc, w których widziałem, jak się go używa. =)
Zaibot,

FYI: ta odpowiedź została tu scalona ze stackoverflow.com/questions/4574940/…
Shog9,

2
polega na odroczeniu wykonania kodu w osobnej, kolejnej pętli zdarzeń : jak ustalić kolejną pętlę zdarzeń ? Jak ustalić, co to jest bieżąca pętla zdarzeń? Skąd wiesz, w którym teraz jesteś etapie pętli zdarzeń?
Zielony,

@ Zielona cóż, naprawdę nie; tak naprawdę nie ma bezpośredniego wglądu w to, co robi środowisko wykonawcze JavaScript.
Pointy

requestAnimationFrame rozwiązał problem z IE i Firefoksem, które czasami nie aktualizowały interfejsu użytkownika
David

9

To stare pytania ze starymi odpowiedziami. Chciałem dodać nowe spojrzenie na ten problem i odpowiedzieć, dlaczego tak się dzieje, a nie dlaczego jest to przydatne.

Masz więc dwie funkcje:

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

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

a następnie zadzwoń do nich w następującej kolejności f1(); f2(); aby zobaczyć, że drugi został wykonany jako pierwszy.

I oto dlaczego: nie można mieć setTimeoutopóźnienia 0 milisekund. Wartość minimalna jest określona przez przeglądarkę i nie jest 0 milisekund. Historycznie przeglądarki ustawiają to minimum na 10 milisekund, ale specyfikacje HTML5 i nowoczesne przeglądarki ustawiają to na 4 milisekundy.

Jeśli poziom zagnieżdżenia jest większy niż 5, a limit czasu jest mniejszy niż 4, zwiększ limit czasu do 4.

Również z mozilli:

Aby zaimplementować limit czasu 0 ms w nowoczesnej przeglądarce, możesz użyć window.postMessage () jak opisano tutaj .

Informacje PS są pobierane po przeczytaniu następującego artykułu .


1
@ user2407309 Żartujesz? masz na myśli, że specyfikacja HTML5 jest nieprawidłowa i masz rację? Przeczytaj źródła, zanim zagłosujesz i zrób poważne roszczenia. Moja odpowiedź oparta jest na specyfikacji HTML i zapisie historycznym. Zamiast odpowiedzi, która wyjaśnia ciągle to samo, dodawałem coś nowego, coś, czego nie było w poprzednich odpowiedziach. Nie mówię, że to jedyny powód, po prostu pokazuję coś nowego.
Salvador Dali

1
Jest to niepoprawne: „A oto dlaczego: nie można ustawić parametru SetTimeout z opóźnieniem 0 milisekund”. Nie dlatego. Opóźnienie 4 ms nie ma znaczenia, dlaczego setTimeout(fn,0)jest użyteczne.
Vladimir Kornea

@ user2407309 można go łatwo zmienić na „aby dodać do powodów podanych przez innych, nie jest to możliwe…”. Dlatego niedorzeczne jest głosowanie z tego powodu, szczególnie jeśli twoja odpowiedź nie mówi nic nowego. Wystarczy niewielka edycja.
Salvador Dali

10
Salvador Dali: Jeśli zignorujesz emocjonalne aspekty wojny o mikro płomienie, prawdopodobnie będziesz musiał przyznać, że @VladimirKornea ma rację. Prawdą jest, że przeglądarki odwzorowują opóźnienie 0 ms na 4 ms, ale nawet gdyby tak nie było, wyniki byłyby takie same. Mechanizmem napędowym jest tutaj to, że kod jest wypychany do kolejki zamiast stosu wywołań. Rzuć okiem na tę doskonałą prezentację JSConf, która może pomóc w wyjaśnieniu problemu: youtube.com/watch?v=8aGhZQkoFbQ
hashchange

1
Jestem zdezorientowany, dlaczego uważasz, że twoja oferta na kwalifikowane minimum 4 ms jest globalnym minimum 4 ms. Jako cytat z pokazów HTML5 Spec minimalna wynosi 4 ms tylko wtedy, gdy masz zagnieżdżone połączenia do setTimeout/ setIntervalod ponad pięciu poziomów głębokości; jeśli tego nie zrobisz, minimum wynosi 0 ms (co w przypadku braku wehikułów czasu). Dokumenty Mozilli rozszerzają tę setIntervalfunkcję , aby objąć powtarzające się, nie tylko zagnieżdżone przypadki (więc z interwałem 0 natychmiast ponownie zaplanuje kilka razy, a następnie opóźni później), ale proste zastosowania setTimeoutz minimalnym zagnieżdżaniem mogą natychmiast ustawiać się w kolejce.
ShadowRanger

8

Ponieważ jest przekazywany przez czas trwania 0, przypuszczam, że ma to na celu usunięcie kodu przekazanego do setTimeoutprzepływu wykonywania. Więc jeśli jest to funkcja, która może chwilę potrwać, nie uniemożliwi to wykonania następnego kodu.


FYI: ta odpowiedź została tu scalona ze stackoverflow.com/questions/4574940/…
Shog9,

7

Obie te najwyżej ocenione odpowiedzi są błędne. Sprawdź opis MDN na modelu współbieżności i pętli zdarzeń , a powinno być jasne, co się dzieje (ten zasób MDN to prawdziwy klejnot). I po prostu za pomocą setTimeout może dodawać nieoczekiwane problemy w kodzie oprócz „rozwiązania” tego małego problemu.

Co właściwie dzieje się tutaj, nie polega na tym, że „przeglądarka może nie być jeszcze całkiem gotowa, ponieważ współbieżność” lub coś w oparciu o „każda linia jest zdarzeniem dodawanym z tyłu kolejki”.

jsfiddle dostarczone przez DVK rzeczywiście ilustruje problem, ale jego wyjaśnienie nie jest poprawna.

To, co dzieje się w jego kodzie, polega na tym, że najpierw dołącza moduł obsługi zdarzeń do clickzdarzenia w sieci#do przycisku.

Następnie, kiedy faktycznie klikniesz przycisk, messagetworzony jest a, odwołujący się do funkcji obsługi zdarzeń, która jest dodawana do message queue. Gdy event looposiągnie ten komunikat, tworzy framestos na stosie, z wywołaniem funkcji do modułu obsługi zdarzeń kliknięcia w jsfiddle.

I tu robi się ciekawie. Jesteśmy tak przyzwyczajeni do myślenia o JavaScript jako asynchronicznym, że mamy tendencję do przeoczania tego drobnego faktu: każda ramka musi zostać wykonana w całości, zanim będzie można wykonać następną ramkę . Brak współbieżności, ludzie.

Co to znaczy? Oznacza to, że ilekroć funkcja jest wywoływana z kolejki komunikatów, blokuje ona kolejkę, dopóki stos, który generuje, nie zostanie opróżniony. Lub, bardziej ogólnie, blokuje się, dopóki funkcja nie powróci. I blokuje wszystko , w tym operacje renderowania DOM, przewijanie i tak dalej. Jeśli chcesz potwierdzić, po prostu spróbuj zwiększyć czas trwania długiej operacji w skrzypcach (np. Uruchom pętlę zewnętrzną jeszcze 10 razy), a zauważysz, że podczas jej działania nie można przewijać strony. Jeśli działa wystarczająco długo, przeglądarka zapyta, czy chcesz zabić proces, ponieważ powoduje to brak reakcji strony. Ramka jest wykonywana, a pętla zdarzeń i kolejka komunikatów zostają zablokowane, dopóki się nie zakończy.

Dlaczego więc ten efekt uboczny tekstu się nie aktualizuje? Ponieważ podczas nie zmienił wartość elementu w DOM - można console.log()jego wartość natychmiast po zmianie go i zobaczyć, że nie zostały zmienione (co pokazuje, dlaczego wyjaśnienie DVK nie jest poprawna) - przeglądarka czeka na stosie wyczerpywać ( onfunkcja modułu obsługi do zwrócenia), a zatem komunikat do końca, aby mógł w końcu przejść do wykonania komunikatu dodanego przez środowisko wykonawcze w odpowiedzi na naszą operację mutacji oraz w celu odzwierciedlenia tej mutacji w interfejsie użytkownika .

Jest tak, ponieważ faktycznie czekamy na zakończenie działania kodu. Nie powiedzieliśmy: „ktoś to pobiera, a potem wywołuje tę funkcję z wynikami, dziękuję, a teraz już gotowe, więc wracam, rób cokolwiek teraz”, jak to zwykle robimy z naszym asynchronicznym skryptem JavaScript opartym na zdarzeniach. Wchodzimy w funkcję obsługi zdarzenia kliknięcia, aktualizujemy element DOM, wywołujemy inną funkcję, inna funkcja działa przez długi czas, a następnie wraca, następnie aktualizujemy ten sam element DOM, a następnie wracamy z funkcji początkowej, skutecznie opróżniając stos. A następnie przeglądarka może dostać się do następnej wiadomości w kolejce, które mogłyby równie dobrze być komunikat wygenerowany przez nas przez wywołanie niektóre wewnętrzne „on-DOM-mutacji” typu imprezy.

Interfejs przeglądarki nie może (lub zdecyduje się nie aktualizować), dopóki bieżąca ramka nie zostanie zakończona (funkcja powróciła). Osobiście uważam, że jest to raczej zgodne z projektem niż z ograniczeniami.

Dlaczego więc to setTimeoutdziała? Robi to, ponieważ skutecznie usuwa wywołanie długo działającej funkcji ze swojej ramki, planując wykonanie jej później w windowkontekście, aby sam mógł natychmiast powrócić i pozwolić kolejce komunikatów na przetwarzanie innych komunikatów. Pomysł polega na tym, że komunikat „przy aktualizacji” interfejsu użytkownika, który został wywołany przez nas w JavaScript podczas zmiany tekstu w DOM, jest teraz przed komunikatem w kolejce dla funkcji długo działającej, aby aktualizacja interfejsu użytkownika miała miejsce przed zablokowaniem przez długi czas.

Zwróć uwagę, że a) długo działająca funkcja nadal blokuje wszystko podczas działania, i b) nie masz gwarancji, że aktualizacja interfejsu użytkownika faktycznie wyprzedza ją w kolejce komunikatów. W mojej przeglądarce Chrome z czerwca 2018 r. Wartość 0„nie” rozwiązuje problemu, który pokazuje skrzypce - 10. Właściwie jestem trochę tym zdławiony, ponieważ wydaje mi się logiczne, że komunikat o aktualizacji interfejsu użytkownika powinien być w kolejce przed nim, ponieważ jego wyzwalacz jest wykonywany przed zaplanowaniem, aby długo działająca funkcja była uruchamiana „później”. Ale być może istnieją pewne optymalizacje w silniku V8, które mogą przeszkadzać, a może po prostu brakuje mi zrozumienia.

Okej, więc jaki jest problem z używaniem setTimeouti jakie jest lepsze rozwiązanie dla tego konkretnego przypadku?

Po pierwsze, problem z używaniem setTimeoutw jakiejkolwiek procedurze obsługi zdarzeń w celu złagodzenia innego problemu jest podatny na bałagan z innym kodem. Oto prawdziwy przykład z mojej pracy:

Kolega, w błędnym rozumieniu pętli zdarzeń, próbował „powiązać” JavaScript, używając setTimeout 0do renderowania kodu do renderowania szablonu . Nie ma go tutaj, by zapytać, ale mogę założyć, że być może wstawił liczniki czasu, aby zmierzyć szybkość renderowania (co byłoby natychmiastową natychmiastową funkcją powrotu) i stwierdził, że zastosowanie tego podejścia spowodowałoby niezwykle szybkie odpowiedzi z tej funkcji.

Pierwszy problem jest oczywisty; nie możesz wątkować w javascript, więc nic nie wygrywasz, dodając zaciemnianie. Po drugie, teraz skutecznie oddzieliłeś renderowanie szablonu od stosu możliwych detektorów zdarzeń, które mogą oczekiwać, że ten sam szablon zostanie wyrenderowany, podczas gdy może nie być. Rzeczywiste zachowanie tej funkcji było teraz niedeterministyczne, podobnie jak - nieświadomie - każda funkcja, która by ją uruchomiła lub od niej zależała. Możesz zgadywać, ale nie możesz poprawnie zakodować jego zachowania.

„Poprawka” podczas pisania nowego modułu obsługi zdarzeń zależnego od jego logiki również miała użyćsetTimeout 0 . Ale to nie jest poprawka, trudno ją zrozumieć i nie jest fajnie debugować błędy spowodowane przez taki kod. Czasami nigdy nie ma problemu, innym razem konsekwentnie zawodzi, a potem znowu, czasami działa i psuje się sporadycznie, w zależności od aktualnej wydajności platformy i wszystkiego, co dzieje się w tym czasie. Właśnie dlatego osobiście odradzam korzystanie z tego hacka ( jest to hack i wszyscy powinniśmy wiedzieć, że tak jest), chyba że naprawdę wiesz, co robisz i jakie są konsekwencje.

Ale co można zamiast tego zrobić? Cóż, jak sugeruje przywołany artykuł MDN, podziel pracę na wiele wiadomości (jeśli możesz), aby inne wiadomości umieszczone w kolejce mogły być przeplatane z twoją pracą i wykonywane podczas jej działania, lub użyj robota internetowego, który może działać w parze z twoją stroną i zwracaj wyniki po zakończeniu obliczeń.

Aha, a jeśli myślisz: „Cóż, czy nie mógłbym po prostu oddzwonić do funkcji długo działającej, aby uczynić ją asynchroniczną?”, To nie. Oddzwonienie nie czyni go asynchronicznym, nadal będzie musiał uruchomić długo działający kod przed jawnym wywołaniem oddzwonienia.


3

Inną czynnością jest przesunięcie wywołania funkcji na spód stosu, zapobiegając przepełnieniu stosu, jeśli rekurencyjnie wywołujesz funkcję. Ma to efekt whilepętli, ale pozwala silnikowi JavaScript odpalać inne asynchroniczne timery.


Głosuj za to push the function invocation to the bottom of the stack. To stack, o czym mówisz, jest niejasne. Najważniejsze jest to, gdzie dokładnie setTimeoutdodaje tę funkcję, koniec tego cyklu pętli lub sam początek następnego cyklu pętli.
Zielony

1

Dzwoniąc do setTimeout, dajesz stronie czas na reakcję na wszystko, co robi użytkownik. Jest to szczególnie przydatne w przypadku funkcji uruchamianych podczas ładowania strony.


1

Niektóre inne przypadki, w których setTimeout jest przydatny:

Chcesz podzielić długo działającą pętlę lub obliczenia na mniejsze elementy, aby przeglądarka nie „zamroziła” się i nie powiedziała „Skrypt na stronie jest zajęty”.

Chcesz wyłączyć przycisk przesyłania formularza po kliknięciu, ale jeśli wyłączysz przycisk w module obsługi onClick, formularz nie zostanie przesłany. setTimeout z czasem zerowym rozwiązuje problem, pozwalając na zakończenie zdarzenia, rozpoczęcie przesyłania formularza, a następnie można wyłączyć przycisk.


2
Wyłączenie lepiej byłoby wykonać w zdarzeniu onsubmit; byłoby to szybsze i gwarantuje się, że zostanie wywołane przed przesłaniem formularza, ponieważ można zatrzymać przesyłanie.
Kris,

Bardzo prawdziwe. Przypuszczam, że wyłączenie onclick jest łatwiejsze do prototypowania, ponieważ możesz po prostu wpisać onclick = "this.disabled = true" w przycisku, podczas gdy wyłączenie po przesłaniu wymaga nieco więcej pracy.
fabspro

1

Odpowiedzi na temat pętli wykonania i renderowania DOM przed uzupełnieniem innego kodu są poprawne. Zero drugich przekroczeń limitu czasu w JavaScript pomaga uczynić kod pseudo-wielowątkowym, nawet jeśli tak nie jest.

Chcę dodać, że NAJLEPSZA wartość limitu czasu zero-sekund w przeglądarce dla wielu przeglądarek / platform między platformami wynosi w rzeczywistości około 20 milisekund zamiast 0 (zero), ponieważ wiele przeglądarek mobilnych nie może zarejestrować limitów czasu krótszych niż 20 milisekund z powodu ograniczeń zegara na układach AMD.

Ponadto długotrwałe procesy, które nie wymagają manipulacji DOM, powinny być teraz przesyłane do pracowników sieci Web, ponieważ zapewniają one prawdziwą wielowątkową obsługę JavaScript.


2
Jestem nieco sceptycznie nastawiony do twojej odpowiedzi, ale głosowałem za nią, ponieważ zmusiło mnie to do przeprowadzenia dodatkowych badań standardów przeglądarki. Badając standardy, idę tam, gdzie zawsze idę, MDN: developer.mozilla.org/en-US/docs/Web/API/window.setTimeout Specyfikacja HTML5 mówi 4ms. Nie mówi nic o ograniczeniach zegara na chipach mobilnych. Trudno jest znaleźć w Google źródło informacji do tworzenia kopii zapasowych swoich oświadczeń. Dowiedziałem się, że Dart Language od Google całkowicie usunął setTimeout na rzecz obiektu Timer.
John Zabroski

8
(...) ponieważ wiele przeglądarek mobilnych nie może zarejestrować limitów czasu krótszych niż 20 milisekund z powodu ograniczeń zegara (...) Każda platforma ma ograniczenia czasowe z powodu swojego zegara i żadna platforma nie jest w stanie wykonać następnej rzeczy dokładnie 0 ms po obecne. Limit czasu 0ms prosi o jak najszybsze wykonanie funkcji, a ograniczenia czasowe konkretnej platformy w żaden sposób nie zmieniają tego znaczenia.
Piotr Dobrogost

1

setTimout na 0 jest również bardzo użyteczny w przypadku konfiguracji odroczonej obietnicy, którą chcesz od razu zwrócić:

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

Problem polegał na tym, że próbujesz wykonać operację JavaScript na nieistniejącym elemencie. Element miał być jeszcze załadowany i setTimeout()daje więcej czasu na załadowanie elementu w następujący sposób:

  1. setTimeout()powoduje, że zdarzenie jest asynchroniczne, dlatego jest wykonywane po całym kodzie synchronicznym, dając Twojemu elementowi więcej czasu na załadowanie. Asynchroniczne wywołania zwrotne, takie jak wywołanie zwrotne, setTimeout()są umieszczane w kolejce zdarzeń i umieszczane na stosie przez pętlę zdarzeń gdy stos kodu synchronicznego jest pusty.
  2. Wartość 0 dla ms jako drugiego argumentu w funkcji setTimeout()jest często nieco wyższa (4-10 ms w zależności od przeglądarki). Ten nieco dłuższy czas potrzebny do wykonania setTimeout()wywołania zwrotnego jest spowodowany ilością „tyknięć” (gdy tyknięcie wypycha wywołanie zwrotne na stosie, jeśli stos jest pusty) w pętli zdarzeń. Ze względu na wydajność i żywotność baterii ilość tyknięć w pętli zdarzeń jest ograniczona do pewnej ilości mniejszej niż 1000 razy na sekundę.

-1

JavaScript jest aplikacją jednowątkową, więc nie pozwala na jednoczesne uruchamianie funkcji, więc do osiągnięcia tego celu stosuje się pętle zdarzeń. Więc dokładnie to, co robi setTimeout (fn, 0), to wkleja się do zadania, które jest wykonywane, gdy stos wywołań jest pusty. Wiem, że to wytłumaczenie jest dość nudne, dlatego polecam przejrzenie tego filmu, który pomoże ci sprawdzić, jak działa przeglądarka pod maską. Sprawdź ten film: - https://www.youtube.com/watch?time_continue=392&v=8aGhZQkoFbQ

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.