Dzisiaj odbędziemy podróż w głąb Javascriptu. I to tak dosłownie, bo pogadamy trochę o silniku JavaScript i całej magii, która dzieje się w naszej przeglądarce, m.in. czym jest call stack, czy jak działa event loop. W ramach wyjaśnienia, nie są to rzeczy, które każdy frontendowiec musi wiedzieć!!! Jednak dzięki temu zrozumiesz, jak w ogóle działa Javascript w przeglądarce i czemu Twój kod wykonuje się w takiej kolejności, a nie innej 😉 . To też może pozwolić Ci na pisanie bardziej optymalnego kodu.
Jednowątkowość w JavaScript
Javascript jest językiem jednowątkowym. Oznacza, to iż w danym momencie może wykonywać tylko jedną czynność. Ale… zaraz, zaraz… pomyślmy teraz o przeglądarkach. Jak odpalamy jakąkolwiek stronę internetową, to przecież tam się dzieje mnóstwo procesów – wysyłanie zapytań, w tym pobieranie obrazków, fontów, styli, nasłuchiwanie na zdarzenia itd.. A jednak nie czekamy na to wszystko kilka minut. Jak to się dzieje? Żeby do tego dojść trzeba sobie wyjaśnić najpierw kilka rzeczy 🙂 .
Serce JavaScriptu
Głównym elementem JavaScriptu jest jego silnik, który wykonuje kod Javascript. I tak w przypadku przeglądarki Chrome jest to V8, Firefoxa – SpiderMonkey, Edge – Chakra, Safari – JavaScriptCore.
Nie będziemy tutaj wchodzić w szczegóły, jak kod jest interpretowany przez ten silnik. Co nas bardziej interesuje to to, iż wszystko, co sobie definiujemy w JavaScripcie, jest gdzieś zapamiętywane. Służą do tego dwie struktury silnika: memory heap (sterta) i call stack (stos wywołań). Każde z nich ma różne zastosowanie.
Heap to obszar pamięci dla obiektów, natomiast stack odnosi się do prymitywnych wartości i funkcji. Największą różnicą jest jednak to, w jaki sposób jest alokowana tam pamięć.
Otóż heap przechowuje dane nieuporządkowanie i rośnie dynamicznie. Tym samym możemy odnieść się do danego elementu i np. go usunąć nie patrząc na kolejność, w jakiej został do tej struktury dodany. Natomiast call stack, związany jest z wykonywaniem funkcji.
O co chodzi z Call Stack ?
Jeśli w kodzie pojawią się wywołania funckji, są one odkładane na stos, następnie zdejmowane z niego i wykonywane. I jak na stos przystało, pierwsze zdejmowane są funkcje, które zostały na niego odłożone jako ostatnie ( zasada LIFO – Last In First Out ).
Powyższa animacja mam nadzieję, iż dobrze pokazuje, jak działa Call Stack. W tym przypadku mamy zobrazowanie tego, jak jedna funkcja wywołuje inną funkcję, stąd są one odkładane kolejno na stos. To tak jakby powiedzieć Javacriptowi, iż mamy dla niego zadania do wykonania. Tylko przez jego jednowątkowość musi najpierw skończyć jedno i dopiero po nim może sobie wziąć kolejne ze stosu.
Btw, main() to funkcja pochodząca od pliku, w którym uruchamiamy nasz kod. W naszej konsoli często jawi się jako anonymous.
I choćby jeżeli nie zdawałaś sobie sprawy ze stosu wywołań, to możesz kojarzyć errory w DevToolsach, które posiadają tzw. stack trace. Jest to ścieżka wywołań, która mówi developerowi, w jakiej kolejności, w której linii i pliku dana funkcja została wywołana.
Możliwe jest zbadanie tego również poprzez wywołanie console.trace() w danej funkcji.
Środowisko przeglądarki
Wyobraźmy teraz sobie sytuację, iż cały nasz kod jest synchroniczny, tj. wykonuje się linijka po linijce. Pierwsze zapytanie jest o dużą ilość danych, wiesz co będzie dalej… ?
Pewnie byśmy sobie poczekali.
Na ratunek przychodzą nam Web APIs, które zapewnia nam przeglądarka. Mowa tu, np. o DOMie i wszelkich zdarzeniach z nim związanych, o AJAX, o timerach (setTimeout, setInterval) oraz requestAnimationFrame. Po wywołaniu tych asynchonicznych funkcji, to przeglądarka bierze na siebie oczekiwanie na ich wykonanie. Jednak samo wykonanie ich należy do zadań JavaScriptu, stąd jakimś cudownym sposobem musi to trafić do Call Stacka. I tutaj na scenę wchodzą: kolejka zadań (task queue) i pętla zdarzeń (event loop). [oklaski]
Działanie Event Loopa
Schemat z naszym silnkiem JavaScriptu poszerzymy sobie o twory przeglądarki i powstaje:
Okej, co my tu mamy? jeżeli w naszym kodzie pojawi się funkcja, która dostarczana jest przez Web API, np. setTimeout(callback, delay) jest ona zdejmowana ze stosu wywołań. Zajmuje się nią przeglądarka. Uruchamia ona timer odliczający czas (delay), po którym można wykonać funkcje (callback). Po tym czasie nasz callback jest wypychany do kolejki zadań.
Zadaniem event loopa jest sprawdzanie czy stos jest pusty i czy coś znajduje się w kolejce. Dopiero w momencie, kiedy w Call Stacku nie ma już zadań, event loop przekazuje do niego zadania z kolejki.
Jak to wygląda w praktyce?
Można by myśleć, iż setTimeout wykona się natychmiast – w końcu ma zerowe opóźnienie. Jednak jak wcześniej napisałam – wykona się dopiero w momencie, kiedy stack będzie pusty. Stąd w outpucie właśnie taka kolejność.
Event Loop zajmuje się też zadaniami związanymi z requestAnimationFrame. Są one ściśle powiązane z animacjami oraz osobną kolejką do nich – kolejką renderowań. Nie będziemy jednak tutaj wchodzić w szczegóły.
Kolejka zadań – czy zadania są sobie równe?
Rozważmy teraz taki o kod:
Idąc od góry mamy console.log(script start'), wykona się ✅ . Drugie setTimeout , wiemy, iż idzie do kolejki zadań, zatem my idziemy dalej. Promise jest asynchroniczne, wykona się później. Pozostaje nam ostatni console.log('script end') ✅. Jaka więc jest dalsza kolejność?
Otóż okazuje się, iż kolejka zadań, to tak naprawdę dwie kolejki 😅 .
W specyfikacji została wydzielona kolejka nazywana PromiseJobs. Jak nazwa wskazuje dotyczy ona promisów. Przyjęło się, iż nazywamy ją kolejką dla mikrozadań/mikrotasków. Zatem jeżeli nasz promise jest gotowy i mamy jakiś handler (jest na to jakieś polskie słowo?) w postaci .then/catch/finally , to trafiają one normalnie do kolejki. Jednak są wykonywane przed makrotaskami (pozostałe zadania, które znajdują się w kolejce).
Kolejka dla mikrotasków:
- działa na zasadzie first-in-first-out, zadania zakolejkowane jako pierwsze, są jako pierwsze również brane
- wykonywanie tasków z tej kolejki jest mozliwe tylko wtedy, gdy stack jest pusty
Skoro to już wiesz, to pewnie znasz odpowiedź, co do powyższego kodu i kolejności wykonania 😉 .
Podsumowanie
Jeśli tutaj dobrnęłaś, to gratuluję! Było intensywnie, co nie? Tak jak zaznaczyłam na początku, nie jest to wiedza konieczna do bycia frontendowcem. Sama niedawno odkryłam część z tego, dlatego podjarana tym faktem, swierdziłam, iż warto się podzielić 😀 . Teraz wiemy więcej co się dzieje w naszym kodzie, a to zawsze krok do przodu! Daj znać jak się podobało!