Asynchroniczny JS pod maską

Jan 27 2023
MDN definiuje asynchroniczny przepływ pracy jako ..

MDN definiuje asynchroniczny przepływ pracy jako

..rozpoczęcie potencjalnie długotrwałego zadania i nadal możliwość reagowania na inne zdarzenia podczas wykonywania tego zadania… Po zakończeniu tego zadania program jest prezentowany z wynikiem.

Ale jak dokładnie można to osiągnąć w Internecie za pomocą JavaScript?

Przed zagłębieniem się w implementacje asynchroniczne warto mieć podstawową wiedzę na temat JS na niższym poziomie.

Jak działa JS?

JavaScript jest jednowątkowy i interpretowany, co oznacza, że ​​instrukcje są wykonywane bezpośrednio i nie mają etapu kompilacji ex-ante. Weźmy jako przykład najpopularniejszy silnik V8.

Na wysokim poziomie V8 analizuje kod, tworzy abstrakcyjne drzewo składni (AST) i kompiluje je do zoptymalizowanego kodu bajtowego za pomocą interpretera. Jest też preparser, dwa kompilatory i buforowanie, które sprawiają, że jest szybki (V8 ma świetne blogi na ten temat). Zapewnia nam tylko stos wywołań, stertę i pętlę zdarzeń ( tak jakby nie do końca) . Możemy zignorować stertę dla celów tej króliczej nory: jest to fragment nieustrukturyzowanej pamięci z odniesieniami do obiektów i zmiennymi.

Pętla zdarzeń w czasie wykonywania

W przeciwieństwie do Javy czy C++, JS pozwala na wykonanie jednego zdarzenia naraz. Możemy to zobaczyć w naszych przeglądarkach, śledząc ścieżkę połączenia za pomocą console.trace():

// shoutout to ES6 arrow functions

foo = () => {
    console.log("Hello, World!");
    bar();
}
bar = () => {
    console.log("Goodbye, World!");
    trace();
}

trace = () => console.trace();

foo();

Hello, World!   VM1049:6
Goodbye, World! VM1049:11 

// console.trace
trace @ VM1049:11
bar @ VM1049:7
foo @ VM1049:3

      
                
call stack visualization

Ustaw limit czasu

Uruchommy te same metody z asynchronicznym opóźnieniem 5000 ms z setTimeout().

foo = () => {
    console.log("Hello, World!");
     setTimeout(bar, 5000);
}

bar = () => {
    console.log("Goodbye, World!");
    trace();
}

trace = () => console.trace();

foo();

Hello, World!   VM1526:2

// 3 days later... 
Goodbye, World!  VM1526:6

// console.trace 
trace @ VM1382:10
bar @ VM1382:6

// setTimeout is invoked
b @ main-base.bundle.UM3…oFn_e5gQ.12.js:1723
setTimeout (async)  
Cga.window.setTimeout @ main-base.bundle.UM3…toFn_e5gQ.12.js:516

// foo is pushed to the stack first
foo @ VM1382:3

setTimeoutnie jest częścią silnika JS, co ma sens, ponieważ silnik może obsłużyć jedno zdarzenie na raz. Przeglądarki są jednak wielowątkowe i zapewniają nam internetowe interfejsy API umożliwiające dostęp do implementacji setTimeout. Na przykład Chrome używa jednowątkowego silnika V8 w architekturze wieloprocesowej, w której każdy proces jest mocno wielowątkowy. W tym przypadku zostało to wyraźnie wywołane za pomocą Cga.setTimeout @ main-base.bundle.blah-blah-blah...Virtually, co umożliwia nam korzystanie z wielu wątków.

Pętla zdarzeń

Zmieńmy kolejność naszego bloku kodu:

foo = () => {
    console.log("Hello, World!");
     setTimeout(bar, 5000);
}

bar = () => {
    setTimeout(() => console.log("Goodbye, World!"), 5000);
    trace();
}

trace = () => console.trace();

foo();

// output of foo 
Hello, World! VM1649:11 

// console.trace 
trace @ VM1694:11
bar @ VM1694:8
b @ main-base.bundle.UM3…oFn_e5gQ.12.js:1723
setTimeout (async)  
Cga.window.setTimeout @ main-base.bundle.UM3…toFn_e5gQ.12.js:516
foo @ VM1694:3
// end trace 

// 3 days later...
VM1649:6 Goodbye, World!

Środowisko JRE obejmuje:

  • Silnik JS (jednowątkowy)
  • Wirtualna maszyna Java (JVM) — Wirtualna maszyna, która wykonuje kod w czasie wykonywania. Przy okazji, właśnie dlatego „VM” dołącza dzienniki śledzenia.
  • Internetowy interfejs API
  • Kolejka zadań
  • Pętla zdarzeń

Stos wywołań naszego silnika wyświetla zdarzenia jedno po drugim. Po wyświetleniu setTimeout() metoda wraz z wywołaniem zwrotnym (referencja do funkcji) jest delegowana do internetowego interfejsu API.

Zauważ, że silnik nie dba już o to, jak implementacja setTimeout interfejsu API obsługuje zdarzenie i wywołanie zwrotne. Silnik przechodzi dalej, aby wyświetlić następne zdarzenie i przekazać je Pacmanowi (maszynie JVM, która wykonuje kod).

W międzyczasie API wykorzystuje swoje wątki do odczekania 5000 ms, a następnie przekazuje je do kolejki zadań. Pętla zdarzeń widzi, że zdarzenie ustawia się w kolejce i wypycha je z powrotem na stos wywołań wtedy i tylko wtedy, gdy stos wywołań jest pusty.

Możesz użyć zabawnego wizualizatora Philipa Robertsa , aby zobaczyć to wszystko w akcji. Zobacz także jego niesamowite przemówienie na JSConf w 2014 roku.