Klasyczne pytanie z rozmów rekrutacyjnych. Co wypisze ten kod?
console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve()
.then(() => console.log('C'))
.then(() => console.log('D'));
queueMicrotask(() => console.log('E'));
(async () => {
console.log('F');
await null;
console.log('G');
})();
console.log('H');Jeśli Twoja pierwsza myśl to "z góry na dół, od A do H", to spokojnie, większość ludzi myśli tak samo. Kod faktycznie wykonuje się z góry na dół. Ale niektóre linie nie uruchamiają swojego callbacka, tylko go planują. Żeby przewidzieć kolejność, potrzebujesz trzech rzeczy.
Trzy elementy układanki
- Stos wywołań. JavaScript ma jeden wątek. Wykonuje to, co jest na stosie, i nigdy nie przerywa tego w połowie. Cały Twój skrypt trafia tam jako pierwszy.
- Kolejka mikrozadań.
.then,.catch,.finally, kod poawaitiqueueMicrotaskwrzucają callbacki właśnie tutaj. Myśl o niej jak o "zaraz po bieżącym kodzie". - Kolejka makrozadań.
setTimeout,setInterval, zdarzenia DOM, sieć i I/O wrzucają callbacki tutaj. Myśl o niej jak o "później".
A spina to wszystko jedna zasada:
Wykonaj kod synchroniczny. Gdy stos jest pusty, wykonaj wszystkie mikrozadania, także te dodane po drodze. Potem weź jedno makrozadanie. Powtórz.
Prześledź to krok po kroku
Najpierw wpisz swoją odpowiedź, potem klikaj dalej. Patrz, co i kiedy ląduje w każdej z kolejek.
Prześledź pętlę zdarzeń krok po kroku
Wstecz i Dalej przesuwają o jeden krok. Działają też klawisze strzałek.
console.log('A'); setTimeout(() => console.log('B'), 0); Promise.resolve() .then(() => console.log('C')) .then(() => console.log('D')); queueMicrotask(() => console.log('E')); (async () => { console.log('F'); await null; console.log('G');})(); console.log('H');Stos wywołańgóra działa teraz
- script
Kolejka mikrozadańPromise, await - najpierw
pustoKolejka makrozadańsetTimeout, zdarzenia - na końcu
pustoKonsola
jeszcze nicSkrypt startuje. Cały plik działa jako jedno zadanie na stosie wywołań, z góry na dół. Obie kolejki są puste.
Trzy rzeczy, na których najłatwiej się potknąć
1. Funkcja async startuje synchronicznie
Wywołanie funkcji async to zwykłe wywołanie funkcji. Wszystko przed pierwszym await wykonuje się od razu. Dlatego F jest drugie, przed H. await dzieli funkcję na dwie części: to, co przed nim, jest synchroniczne, a to, co po nim, zachowuje się jak callback .then.
2. Doczepiony .then nie trafia do kolejki, dopóki nie skończy się poprzedni
.then(D) czeka na promise zwrócony przez .then(C). Ten promise rozstrzyga się dopiero wtedy, gdy C się wykona. D dołącza więc do kolejki dopiero w tym momencie, na jej końcu, za E i G, które czekają tam od czasu wykonania skryptu. Wynik: C E G D, a nie C D E G.
3. setTimeout(fn, 0) nie znaczy "teraz"
Opóźnienie to minimum, a nie gwarancja. Nawet przy 0 callback jest makrozadaniem, więc czeka na kod synchroniczny i na wszystkie mikrozadania. B jest ostatnie.
Ostateczna odpowiedź: A F H C E G D B.
Ćwiczenia
Trzy kolejne scenariusze, od łatwego do podchwytliwego. Najpierw zgadnij, potem prześledź krok po kroku.
Ćwiczenia: zgadnij, potem prześledź krok po kroku
Wybierz scenariusz. Na ostatnim wykłada się wiele osób: setTimeout po await.
console.log('A'); setTimeout(() => console.log('B'), 0); Promise.resolve() .then(() => console.log('C')) .then(() => console.log('D')); queueMicrotask(() => console.log('E')); (async () => { console.log('F'); await null; console.log('G');})(); console.log('H');Stos wywołańgóra działa teraz
- script
Kolejka mikrozadańPromise, await - najpierw
pustoKolejka makrozadańsetTimeout, zdarzenia - na końcu
pustoKonsola
jeszcze nicSkrypt startuje. Cały plik działa jako jedno zadanie na stosie wywołań, z góry na dół. Obie kolejki są puste.
Wersja w jednym zdaniu
Jeśli zapyta o to rekruter, tyle wystarczy:
JavaScript najpierw wykonuje kod synchroniczny, potem opróżnia całą kolejkę mikrozadań (promise, await, queueMicrotask), następnie bierze jedno makrozadanie (timery, zdarzenia, I/O) i powtarza cały cykl. Kolejka zapełnia się w takiej kolejności, w jakiej linie faktycznie się wykonują, a nie w takiej, w jakiej są napisane.
Najważniejsza jest ta ostatnia część. W końcowym scenariuszu setTimeout stoi po await, więc zostaje zarejestrowany dopiero wtedy, gdy funkcja wznawia działanie w fazie mikrozadań. Dlatego jego callback wykonuje się po timerze zapisanym niżej w pliku.