JavaScript pod maską

Spis treści
- Stos wątków i wywołań
- Kontekst wykonania
- Pętla zdarzeń i asynchroniczny JavaScript
- Przechowywanie pamięci i zbieranie śmieci
- Kompilacja JIT (dokładnie na czas).
- Streszczenie
W tym artykule zagłębimy się w wewnętrzne działanie JavaScript i jak on faktycznie działa. Znając szczegóły, zrozumiesz zachowanie swojego kodu i dzięki temu będziesz mógł pisać lepsze aplikacje.
JavaScript jest opisany jako:
Jednowątkowy, bezużyteczny, interpretowany lub skompilowany język programowania Just In Time z nieblokującą pętlą zdarzeń.
Rozpakujmy każde z tych kluczowych terminów.
Stos wątków i wywołań:
Silnik JavaScript to jednowątkowy interpreter składający się ze sterty i pojedynczego stosu wywołań, który jest używany do wykonywania programu.
Stos wywołań to struktura danych, która wykorzystuje zasadę Last In, First Out (LIFO) do tymczasowego przechowywania i zarządzania wywołaniami funkcji.
Oznacza to, że ostatnia funkcja, która zostanie umieszczona na stosie, jest pierwszą, która zostanie wyrzucona po powrocie funkcji.
Ponieważ stos wywołań jest pojedynczy, wykonywanie funkcji odbywa się pojedynczo, od góry do dołu. Oznacza to, że stos wywołań jest synchroniczny.
Teraz, ponieważ jest synchroniczny, będziesz się zastanawiać, jak JavaScript może obsługiwać wywołania asynchroniczne?
Cóż, pętla zdarzeń jest sekretem programowania asynchronicznego w JavaScript.
Ale zanim wyjaśnimy koncepcję wywołań asynchronicznych w JavaScript i jak jest to możliwe w języku jednowątkowym, najpierw zrozummy, jak wykonywany jest kod.
Kontekst wykonania (EC):
Kontekst wykonania jest zdefiniowany jako środowisko, w którym wykonywany jest kod JavaScript.
Tworzenie kontekstu wykonania odbywa się w dwóch fazach:
1. Faza tworzenia pamięci:
- Tworzenie obiektu globalnego (nazywanego obiektem okna w przeglądarce i obiektem globalnym w NodeJS).
- Tworzenie „tego” obiektu i powiązanie go z obiektem globalnym.
- Konfigurowanie sterty pamięci (sterta to duży, w większości nieustrukturyzowany obszar pamięci) do przechowywania zmiennych i odwołań do funkcji.
- Przechowywanie funkcji i zmiennych w globalnym kontekście wykonania poprzez implementację Hoisting .
Teraz, gdy znamy kroki związane z wykonaniem kodu, wróćmy do
Pętla zdarzeń:
Na początek spójrzmy na ten diagram:

Mamy silnik, który składa się z dwóch głównych komponentów:
* Sterta pamięci — tutaj odbywa się alokacja pamięci.
* Call Stack — w tym miejscu znajdują się ramki stosu podczas wykonywania kodu.
Mamy interfejsy API sieci Web, które są wątkami, do których nie masz dostępu, możesz po prostu do nich dzwonić. Są to elementy przeglądarki, w których uruchamia się współbieżność, takie jak DOM, AJAX, setTimeout i wiele innych.
Wreszcie jest kolejka wywołania zwrotnego, która jest listą zdarzeń do przetworzenia. Z każdym zdarzeniem jest powiązana funkcja, która jest wywoływana w celu jego obsługi.
Więc jakie jest tutaj zadanie pętli zdarzeń?
Pętla zdarzeń ma jedno proste zadanie — monitorowanie stosu wywołań i kolejki wywołań zwrotnych. Jeśli stos wywołań jest pusty, pętla zdarzeń pobierze pierwsze zdarzenie z kolejki i przekaże je do stosu wywołań, który skutecznie je uruchomi.
Taka iteracja nazywana jest tikiem w pętli zdarzeń. Każde zdarzenie jest tylko wywołaniem zwrotnym funkcji.
Przechowywanie pamięci i zbieranie śmieci:
Aby zrozumieć potrzebę wyrzucania elementów bezużytecznych, musimy najpierw zrozumieć cykl życia pamięci, który jest prawie taki sam dla każdego języka programowania i składa się z 3 głównych etapów.
1. Przydziel pamięć.
2. Użyj przydzielonej pamięci do odczytu, zapisu lub obu.
3. Zwolnij przydzieloną pamięć, gdy nie jest już potrzebna.
Większość problemów z zarządzaniem pamięcią występuje, gdy próbujemy zwolnić przydzieloną pamięć. Głównym problemem, który się pojawia, jest określenie niewykorzystanych zasobów pamięci.
W przypadku języków niskiego poziomu, w których programista musi ręcznie decydować, kiedy pamięć nie jest już potrzebna, języki wysokiego poziomu, takie jak JavaScript, wykorzystują zautomatyzowaną formę zarządzania pamięcią znaną jako Garbage Collection (GC).
JavaScript wykorzystuje dwie słynne strategie do wykonywania GC: technikę liczenia referencji i algorytm Mark-and-sweep.
Oto szczegółowe wyjaśnienie MDN dotyczące obu algorytmów i sposobu ich działania.
Kompilacja JIT (dokładnie na czas):
Wróćmy do definicji JavaScript: mówi „Interpretowany język programowania skompilowany przez JIT”, więc co to w ogóle oznacza? A może zaczniesz od ogólnej różnicy między kompilatorem a tłumaczem?
Jako analogię pomyśl o dwóch osobach posługujących się różnymi językami, które chcą się porozumieć. Kompilacja jest jak zatrzymanie się i poświęcenie całego czasu na naukę języka, a tłumaczenie będzie jak obecność kogoś, kto zinterpretuje każde zdanie.
Tak więc języki skompilowane mają wolny czas zapisu i szybki czas wykonywania, a języki interpretowane mają odwrotny skutek.
Mówiąc terminami technicznymi: kompilacja to proces przekształcania kodu źródłowego programu w kod binarny do odczytu maszynowego przed wykonaniem, a kompilator pobiera cały program za jednym razem.
Z drugiej strony interpreter to program, który wykonuje instrukcje programu bez konieczności ich wstępnej kompilacji do formatu czytelnego dla komputera, i wymaga pojedynczego wiersza kodu na raz.
I tu pojawia się rola kompilatora JIT, który poprawia wydajność interpretowanych programów. Cały kod jest natychmiast konwertowany na kod maszynowy i natychmiast wykonywany .

Wewnątrz kompilatora JIT mamy nowy komponent zwany monitorem (inaczej profilerem). Ten monitor obserwuje kod podczas jego działania i
- Zidentyfikuj gorące lub ciepłe elementy kodu, np. powtarzający się kod.
- Przekształć te komponenty w kod maszynowy w czasie wykonywania.
- Zoptymalizuj wygenerowany kod maszynowy.
- Wymień na gorąco poprzednią implementację kodu.
Teraz, gdy zrozumieliśmy podstawowe koncepcje, poświęćmy chwilę na złożenie wszystkiego i podsumowanie kroków, które wykonuje JS Engine podczas wykonywania kodu:

- JS Engine pobiera kod JS napisany w składni czytelnej dla człowieka i zamienia go na kod maszynowy.
- Silnik używa parsera, aby przejść przez kod linia po linii i sprawdzić, czy składnia jest poprawna. Jeśli wystąpią jakiekolwiek błędy, kod zostanie zatrzymany i zostanie zgłoszony błąd.
- Jeśli wszystkie kontrole zakończą się pomyślnie, parser tworzy drzewiastą strukturę danych zwaną abstrakcyjnym drzewem składni (AST).
- AST to struktura danych reprezentująca kod w strukturze przypominającej drzewo. Łatwiej jest przekształcić kod w kod maszynowy z AST.
- Następnie tłumacz bierze AST i przekształca go w IR, który jest abstrakcją kodu maszynowego i pośrednikiem między kodem JS a kodem maszynowym. IR pozwala również na optymalizacje i jest bardziej mobilny.
- Następnie kompilator JIT pobiera wygenerowane IR i przekształca je w kod maszynowy, kompilując kod, uzyskując informacje zwrotne w locie i wykorzystując je do usprawnienia procesu kompilacji.
Dziękuję za przeczytanie :)
Możesz śledzić mnie na Twitterze i LinkedIn .