banner
SlhwSR

SlhwSR

热爱技术的一名全栈开发者
github
bilibili

事件ループの原理

一、はじめに#

まず、JavaScriptはシングルスレッドの言語であり、同時に一つのことしかできないことを意味しますが、シングルスレッドであることはブロッキングを意味しません。シングルスレッドの非ブロッキングの方法はイベントループです。

JavaScriptでは、すべてのタスクを次のように分類できます。

  • 同期タスク:即座に実行されるタスクで、通常はメインスレッドで直接実行されます。

  • 非同期タスク:非同期に実行されるタスク、例えばajaxネットワークリクエスト、setTimeoutタイマー関数など

同期タスクと非同期タスクの実行フローチャートは以下の通りです:

image

上記の図からわかるように、同期タスクはメインスレッド、つまりメイン実行スタックに入り、非同期タスクはタスクキューに入ります。メインスレッド内のタスクが完了すると、対応するタスクをタスクキューから読み取り、メインスレッドにプッシュします。このプロセスはイベントループと呼ばれます。

二、マクロタスクとマイクロタスク#

タスクを同期タスクと非同期タスクに分けることは必ずしも正確ではありません。例を挙げましょう:

console.log(1)

setTimeout(()=>{
    console.log(2)
}, 0)

new Promise((resolve, reject)=>{
    console.log('new Promise')
    resolve()
}).then(()=>{
    console.log('then')
})

console.log(3)

上記のコードをフローチャートで分析すると、次の実行手順が得られます:

  • console.log(1):同期タスクであり、メインスレッドで実行されます。
  • setTimeout():非同期タスクであり、Event Tableに配置され、0 ミリ秒後にconsole.log(2)コールバックがEvent Queueにプッシュされます。
  • new Promise:同期タスクであり、メインスレッドで直接実行されます。
  • .then:非同期タスクであり、Event Tableに配置されます。
  • console.log(3):同期タスクであり、メインスレッドで実行されます。

したがって、分析によると、結果は1 => 'new Promise' => 3 => 2 => 'then'となるはずです。

しかし、実際の結果は1 => 'new Promise' => 3 => 'then' => 2となります。

この分岐の原因は非同期タスクの実行順序です。イベントキューは実際には「先入れ先出し」のデータ構造であり、前にあるイベントがメインスレッドによって優先的に読み取られます。

例では、setTimeoutのコールバックイベントが最初にキューに入るべきですが、.thenのコールバックイベントが先にキューに入ってしまいます。

その理由は、非同期タスクはマイクロタスクとマクロタスクにさらに分類できるからです。

マイクロタスク#

非同期に実行する関数であり、メイン関数の実行が終了した後、現在のマクロタスクが終了する前に実行されます。

一般的なマイクロタスクには次のものがあります:

  • Promise.then

  • MutaionObserver

  • Object.observe(非推奨;Proxy オブジェクトに置き換え)

  • process.nextTick(Node.js)

マクロタスク#

マクロタスクは比較的大きなタイムスパンで実行され、実行間隔を正確に制御することはできません。リアルタイム性の高い要件にはあまり適していません。

一般的なマクロタスクには次のものがあります:

  • script(外部同期コードとして理解できます)
  • setTimeout/setInterval
  • UI レンダリング / UI イベント
  • postMessage、MessageChannel
  • setImmediate、I/O(Node.js)

この時点で、イベントループ、マクロタスク、マイクロタスクの関係は次の図のようになります。

image

このフローに従って、実行メカニズムは次のようになります。

  • マクロタスクを実行し、マイクロタスクがあればマイクロタスクのイベントキューに入れます。
  • 現在のマクロタスクが完了したら、マイクロタスクのイベントキューをチェックし、すべてのマイクロタスクを順番に実行します。

上記の問題に戻りましょう。

console.log(1)
setTimeout(()=>{
    console.log(2)
}, 0)
new Promise((resolve, reject)=>{
    console.log('new Promise')
    resolve()
}).then(()=>{
    console.log('then')
})
console.log(3)

フローは次のようになります。

// console.log(1)に遭遇し、1を直接出力します
// タイマーに遭遇し、後で実行するためにマクロタスクに入れます
// new Promiseに遭遇し、これは直接実行され、'new Promise'を出力します
// .thenはマイクロタスクであり、マイクロタスクキューに入れます
// console.log(3)に遭遇し、3を直接出力します
// これで、現在のマクロタスクが完了しました。今度はマイクロタスクリストをチェックして、.thenのコールバックを実行し、'then'を出力します
// 一度マクロタスクが完了したら、新しいマクロタスクを実行します。ここではタイマーのマクロタスクのみが残っていますので、それを実行し、2を出力します

三、async と await#

asyncは非同期を意味し、awaitasync waitと解釈できます。つまり、asyncは非同期メソッドを宣言するために使用され、awaitは非同期メソッドの実行を待機するために使用されます。

async#

async関数はpromiseオブジェクトを返します。次の 2 つの方法は同等です。

function f() {
    return Promise.resolve('TEST');
}

// asyncFはfと同等です!
async function asyncF() {
    return 'TEST';
}

await#

通常、awaitの後にはPromiseオブジェクトがあり、そのオブジェクトの結果を返します。Promiseオブジェクトでない場合は、その値を直接返します。

async function f(){
    // 同じ意味です
    // return 123
    return await 123
}
f().then(v => console.log(v)) // 123

awaitの後に続くものに関係なく、awaitは後続のコードをブロックします。

async function fn1 (){
    console.log(1)
    await fn2()
    console.log(2) // ブロック
}

async function fn2 (){
    console.log('fn2')
}

fn1()
console.log(3)

上記の例では、awaitは下のコードをブロックします(つまり、マイクロタスクキューに追加します)。同期コードを実行してから、ブロックされたコードを実行します。

したがって、上記の出力結果は1fn232です。

四、フロー分析#

上記の理解に基づいて、JavaScript のさまざまなシナリオの実行順序を大まかに把握することができます。

以下にコードを示します:

async function async1() {
    console.log('async1 start')
    await async2()
    console.log('async1 end')
}
async function async2() {
    console.log('async2')
}
console.log('script start')
setTimeout(function () {
    console.log('settimeout')
})
async1()
new Promise(function (resolve) {
    console.log('promise1')
    resolve()
}).then(function () {
    console.log('promise2')
})
console.log('script end')

分析プロセス:

  1. コード全体を実行し、console.log('script start')に遭遇し、結果を直接出力します。出力はscript startです。
  2. タイマーに遭遇し、マクロタスクとして保留します。
  3. async1()に遭遇し、async1関数を実行し、console.log('async1 start')を出力します。次に、awaitに遭遇し、どうすればよいか?まず、async2を実行し、console.log('async2')を出力します。次に、下のコードをブロックします(つまり、マイクロタスクリストに追加します)。同期コードの実行を中断し、同期コードの実行が完了したら、ブロックされたコードを再度実行します。
  4. new Promiseに移動し、直接実行し、console.log('promise1')を出力します。次に、.then()に遭遇し、マイクロタスクとしてマイクロタスクリストに入れます。
  5. 最後の行を直接出力し、console.log('script end')を出力します。これで同期コードの実行が完了し、マイクロタスクを実行します。つまり、awaitの下のコードを実行し、console.log('async1 end')を出力します。
  6. 次に、次のマイクロタスクを実行します。つまり、.then()のコールバックを実行し、console.log('promise2')を出力します。
  7. 前のマクロタスクが完了したので、次のマクロタスクを実行します。これはタイマーのマクロタスクのみですので、それを実行し、console.log('settimeout')を出力します。

したがって、最終的な結果は次のとおりです:script startasync1 startasync2promise1script endasync1 endpromise2settimeout

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。