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 click
zdarzenia w sieci#do
przycisku.
Następnie, kiedy faktycznie klikniesz przycisk, message
tworzony jest a, odwołujący się do funkcji obsługi zdarzeń, która jest dodawana do message queue
. Gdy event loop
osiągnie ten komunikat, tworzy frame
stos 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ć ( on
funkcja 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 setTimeout
działa? Robi to, ponieważ skutecznie usuwa wywołanie długo działającej funkcji ze swojej ramki, planując wykonanie jej później w window
kontekś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 setTimeout
i jakie jest lepsze rozwiązanie dla tego konkretnego przypadku?
Po pierwsze, problem z używaniem setTimeout
w 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 0
do 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.