内部の非同期 JS

Jan 27 2023
MDN では、非同期ワークフローを次のように定義しています。

MDN では、非同期ワークフローを次のように定義しています。

..長時間実行される可能性のあるタスクを開始し、そのタスクの実行中に他のイベントに応答できるようにする...そのタスクが完了すると、プログラムに結果が表示されます。

しかし、これは JavaScript を使用して Web 上でどのように実現できるのでしょうか?

非同期の実装に入る前に、下位レベルでの JS の基本を理解しておくと役立ちます。

JSはどのように機能しますか?

JavaScript はシングルスレッドで解釈されます。つまり、命令は直接実行され、事前のコンパイル手順はありません。最も人気のあるエンジンである V8 を例に挙げてみましょう。

高レベルでは、V8 はコードを解析し、抽象構文ツリー (AST) を作成し、インタープリターを使用してそれを最適化されたバイトコードにコンパイルします。また、プリパーサー、2 つのコンパイラー、および高速化するキャッシュもあります (V8 には、これに関する素晴らしいブログ があります)。これは、呼び出しスタック、ヒープ、およびイベント ループ (実際にはそうではありません) を提供するだけです。このウサギの穴の目的では、ヒープは無視できます。ヒープは、オブジェクト参照と変数を含む非構造化メモリの塊です。

ランタイムイベントループ

Java や C++ とは異なり、JS では一度に 1 つのイベントを実行できます。これは、次のように呼び出しパスをトレースすることでブラウザで確認できます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

SetTimeout

を使用して、5000 ミリ秒の非同期遅延で同じメソッドを実行してみましょう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

setTimeoutは JS エンジンの一部ではありませんが、エンジンは一度に 1 つのイベントを処理できるため、これは当然のことです。ただし、ブラウザはマルチスレッドであり、setTimeout 実装にアクセスするための Web API を提供します。たとえば、Chrome はマルチプロセス アーキテクチャでシングルスレッド V8 エンジンを使用しており、すべてのプロセスが高度にマルチスレッド化されています。この場合、これはCga.setTimeout @ main-base.bundle.blah-blah-blah...Virtually を使用して明示的に呼び出されており、これにより複数のスレッドを使用できるようになります。

イベントループ

コードブロックを並べ替えてみましょう。

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!

JRE には以下が含まれます。

  • JS エンジン (シングルスレッド)
  • Java 仮想マシン (JVM) —ランタイムでコードを実行する仮想マシン。ところで、トレース ログの先頭に「VM」が付加されるのはこれが理由です。
  • ウェブAPI
  • タスクキュー
  • イベントループ

私たちのエンジンのコールスタックはイベントを 1 つずつポップしています。setTimeout() が見つかると、メソッドはコールバック (関数参照) とともに Web API に委任されます。

エンジンは、API の setTimeout 実装がイベントとコールバックをどのように処理するかを気にしなくなっていることに注意してください。エンジンは次のイベントをポップし、それを Pacman (コードを実行する JVM) にフィードします。

その間、API はスレッドを使用して 5000 ミリ秒待機し、その後タスク キューに渡します。イベント ループは、イベントが整列したことを確認し、コール スタックが空の場合にのみ、イベントをコール スタックにプッシュして戻します。

Philip Roberts の楽しいビジュアライザーを使用して、すべての動作を確認できます。2014 年の JSConf での彼の素晴らしい講演もご覧ください。