JSイベントループの視覚的な解釈とノードイベントループとブラウザイベントループの違い

技術的背景

JavaScript イベント ループ (イベント ループ) は、効率的な非同期操作を実現できるシングルスレッド言語の中核基盤です。js をより深く理解したい場合は、イベント ループを明確に理解する必要があります。nodeの登場以降、jsの動作環境はブラウザ単体ではなくなり、同様にnode内でもイベントループが発生します。では、イベントループとは一体何なのでしょうか? イベント ループに関する記事は数多くありますが、初心者にとっては少しわかりにくいと思われるため、この記事ではイベント ループをより視覚的に表現することを目的として、イベント ループを表面からマクロまで理解することを目的としています。まず背景的な質問をいくつかします。

いくつかの背景的な質問:

1. スレッド、プロセスとは何ですか、この 2 つの違いと関係は何ですか。

プロセスはコンピュータシステムのリソース割り当ての最小単位であり、最小単位には互いに独立したメモリがあり、プロセスには独立したメモリ空間があります。

スレッドは、コンピュータのCPU のスケジューリングと割り当ての最小単位です。最小スケジューリング単位は、CPU 内で独立して実行できる最小の基本単位です。スレッドには独自のメモリ空間はありません。

プロセスには多数のスレッドを含めることができ、各スレッドは異なるタスクを並行して実行します。

視覚的な理解:

コンピュータを会社として考える

プロセスは独立した部門であり、各部門には独自のリソースがあります。

スレッドは各部門の従業員であり、各従業員はリソースを持たず、部門のリソースを共有する最小の作業単位です。

2. js がシングルスレッドである理由

これは主に js の使用に関係しており、node が登場する前は、js はブラウザのスクリプト言語として主にユーザーとブラウザ間の対話を実現し、dom を操作するために使用されていました。シングルスレッドのみである必要があります。そうでない場合は、非常に複雑な同期問題が発生します。

たとえば、js がマルチスレッドで設計されている場合、あるスレッドが dom 要素を変更したい場合、別のスレッドが dom 要素を削除したい場合、ブラウザは途方に暮れて途方に暮れてしまいます...

シングルスレッドとイベントループ(イベントループ)

JavaScript はシングルスレッドであり、コード実行時に順次実行することしかできません。コード実行のブロックを解決するために、js は非同期です。たとえば、setTimeout が発生した場合、タイマー コンテンツが実行された後は実行されません。コードですが、最初にコードを実行し、時間が経過したらタイマーを実行します。

この非同期メカニズムに基づいて、JavaScript にはコードをブロックすることなく効率的に実行できるようにするためのコード実行用の独自のルール セットがあり、このルールがイベント ループです。

ノードとブラウザはどちらも js の実行環境を提供しますが、この 2 つの動作メカニズムは少し異なります。

ブラウザjsの動作仕組み

ブラウザごとに異なる JS エンジンが搭載されています

ブラウザは異なりますが、内部イベント ループ ルールは一貫しています。

node.jsの動作メカニズム

Node.js は js の解析エンジンとして v8 を使用し、I/O 処理には libuv を使用します。

libuv ライブラリはノード API の実行を担当し、さまざまなタスクをさまざまなスレッドに割り当ててイベント ループを形成します。実行結果を非同期で V8 エンジンに返します。

ブラウザのイベントループ

イベント ループを直接言葉で説明するのはより頭を使うため、まずイベント ループに関連する概念を紹介します。

イベント ループには実行実行スタックとイベント キューはイベント ループ内のイベントを格納するためのアドレスであり、マイクロタスクとマクロタスクはイベント ループ内で実行されるイベントです。

実行スタック:

jsのコード全体が読み込まれた後、実行が開始されますが、その際に実行コンテキスト(context)が生成され、コードの実行後に実行コンテキストが解放されます。実行コンテキストの場合、プライベート スコープ、現在のスコープ内の変数、上位スコープ、および現在のスコープ オブジェクト this を含む、現在の JS実行環境と呼ぶこともできます

jsはシングルスレッドなので、前のコードが実行されると、次のコードが実行を待っていることになりますが、このとき、この部分のコード(関数や直接実行可能なコード)は、実行スタックと呼ばれるスタックに置かれます

イベントキュー:

上記の js 操作は同期イベントのみを考慮します。js の実行中に非同期イベント (またはタイミング イベント) が発生すると、対応するイベントは一時停止され、js はこのイベントを現在の実行スタックとは異なる別のキューに追加します。現在の実行コンテキストの同期コード非同期イベントを格納するこのキューは、イベント キューと呼ばれます。

実行環境の実行スタックがクリアされた後、js はその時点でイベント キューが空かどうかを確認し、空でない場合はこれらのイベントの実行を続行します。

イベント キュー内のイベントを実行するときは、実行スタックのルールに従います。まず、現在のイベントに対応する実行コンテキストが生成され、次に実行スタックとイベント キューが生成されます。実行環境が実行されて結果が返されると、js は実行環境を終了して実行環境を破棄し、前のメソッドの実行環境に戻ります。次に、同じルールでイベント キュー内の次のイベントを実行します。イベント キューが空になると、外部の実行環境が破棄され、実行が終了します。

上記の分析から、イベント ループとは、外側の実行スタックのルールを繰り返し、レイヤーごとに深くなってサイクルを形成するイベント キュー内のコードの実行を指すことがわかります。

2つの注意点:

1. 同期タスクは、同じ実行コンテキスト内で非同期タスクより優先されます。

2. 異なる実行環境での非同期タスクの実行順序は、イベント キューに追加された順序によって決まります。

3. イベントキューは異なる実行環境でも同じです

例:

function a(){
    console.log('a')
    setTimeout( () => {
        console.log('a1')
    },0)
}


function b(){
    console.log('b')
    setTimeout( () => {
        console.log('b1')
    },0)
}


setTimeout( () => {
    console.log('上层')
},0)

a()
b()

// 运行结果
// a
// b

// 上层
// a1
// b1

変更時間

function a(){
    console.log('a')
    setTimeout( () => {
        console.log('a1')
    },100)
}


function b(){
    console.log('b')
    setTimeout( () => {
        console.log('b1')
    },0)
}


setTimeout( () => {
    console.log('上层')
},10)

a()
b()

// 运行结果
// a
// b
// b1
// 上层
// a1

イベント ループの視覚的な理解:

js コードのメソッドを病院の患者として考えてみましょう。

実行スタックは順番にすべての患者です

同期タスクは患者を直接訪問します

非同期タスクは、検査を受け、結果を得た後に医師の診察を受ける必要がある患者です。

for ループ

検査が必要な患者さんがグループに属している場合、後ろの列で待っている患者さんは、そのグループの直接診察中の患者さんが全員読み終えた後に次のグループに移ります。

 

マイクロタスク:

  • new Promise()
  • new MutaionObserver()

マクロタスク:

  • setInterval()
  • setTimeout()
  • コード全体
  • I/O操作、UIレンダリング

イベント ループでは、非同期イベントは結果を返し、タスク キューに配置されます。ただし、非同期イベントのタイプに応じて、イベントは実際には対応するマクロタスク キューまたはマイクロタスク キューにキューイングされます。現在の実行スタックが空の場合、メインスレッドはマイクロタスク キューにイベントがあるかどうかを確認します。存在しない場合は、マクロ タスク キューに移動してイベントを取り出し、対応するリターンを現在の実行スタックに追加します。存在する場合は、キュー内のイベントに対応するコールバックが、マイクロ タスク キューが実行されるまで順番に実行されます。タスク キューが空の場合は、マクロ タスクに移動します。キューから最初のイベントを取り出し、対応するコールバックを現在の実行スタックに追加します...というようにループに入ります。

概要:イベントキューはマイクロタスクキューとマクロタスクキューに分かれており、同じイベントサイクル内では常にマイクロタスクがマクロタスクよりも前に実行されます。

視覚的な理解:

マイクロタスクは検査が必要な救急患者です

マクロタスクは検査が必要な一般的な患者です

ノード内のイベントループ

ノードの動作メカニズムから、ibuv ライブラリがノード API の実行を担当していることがわかります。

各段階の意味:

  • setTimeout() timers: このステージは、や など のタイマー キュー内のコールバックを実行します setInterval()
  • I/O コールバック: このフェーズでは、ほぼすべてのコールバックが実行されます。ただし、クローズイベント、タイマー、コールバックは含まれませんsetImmediate()
  • アイドル、準備: このフェーズは内部でのみ使用され、無視できます。
  • ポーリング: 新しい I/O イベントを待機中、ノードはいくつかの特殊な場合にここでブロックされます。
  • check: のコールバックがsetImmediate()この段階で実行されます。
  • close コールバック: たとえば、socket.on('close', ...)この close イベントのコールバック。

実行の順序

v8 エンジンが js コードを解析して libuv エンジンに転送すると、ループは最初にポーリング フェーズに入ります。ポーリング フェーズは同期コード全体の分析に相当し、実行スタックが生成され、ポーリング キューが生成されます。同時にクリアされると、チェック ステージにsetImmediate的回调放入check队列,在setTimeout() 進み 、チェック キューのチェックと実行、タイマー キューのチェックと実行、そしてコールバック ステージに入ってコールバックを実行します。setInterval()定时到期后把其回调事件放入timers队列,チェックとタイマーの順序は固定されておらず、コードが実行される環境の影響を受けます。

新しいステージに入った後は、実行が完了して次のステージに入るまで上記のステージが繰り返されます。

总结:

poll ポーリングは io オブザーバーに属し、process.nextTick() は idle オブザーバーに属し、setImmediate() は check オブザーバーに属します。

ループ チェックの各ラウンドでは、アイドル オブザーバは I/O オブザーバよりも前に配置され、I/O オブザーバはチェック オブザーバよりも優先されます。

最初にコードを入力した時点では、アイドル状態のオブザーバーは存在しません。

ノードのprocess.nextTick()

process.nextTick() はノード内の特別なキューで、各ステージが完了し、次のステージに入る準備ができたときに、これらのイベントが最初に実行されます。つまり、ステージの切り替え時に process.nextTick() が実行されます。また、コールバックがどれほど深くても、コールバックは 1 回実行されます。

約束

上記のステージには Promise は含まれていません. ノード内での Promise はブラウザと同様です. process.nextTick() の後と setTimeout の前に実行されます

例:

const fs = require('fs')
const path = require('path')

const wait = () => new Promise((resolove, reject) => {
  setTimeout(resolove(true), 3)
})
fs.readFile(path.resolve(__dirname, './vue.config.js'), 'utf-8', async (err, data) => {
  console.log('读取的文件内容')
  await wait()
  console.log('测试测试')
  process.nextTick(() => {
    console.log('nextTick')
  })
})

setTimeout(() => {
  console.log('定时器任务0')
}, 0)


setTimeout(() => {
  console.log('定时器任务100')
}, 1000)


setImmediate(() => {
  console.log('立即执行')
})

Promise.resolve().then(() => {
  console.log('promise')
})

process.nextTick(() => {
  console.log('外层nextTick')
})

console.log('外层同步')
// 运行结果
// 外层同步
// 外层nextTick
// promise
// 定时器任务0
// 立即执行
// 读取的文件内容
// 测试测试
// nextTick
// 定时器任务100

視覚的な理解

頑固な探検家 (すべての部屋の端まで行く)

すべてのコードは設定された迷路のようなもので、エンジンは探索に行く人であり、私たちはそれをシャオダイと呼んでいます。

シャオダイは迷路に到着し、すでに地図を持っていたので、地図に従って危険を冒しました。

迷路には 6 つの部屋があります。つまり、タイマー、i/ocallback、ide prepare (内部使用、クローズ)、poll、check、close callback、

その中で、タイマーは仮想現実の部屋であり、Xiaodaiはいつでも中の様子を見ることができます。

他の各部屋には 5 つの部屋があり、開いている部屋と開いていない部屋があります。

遠征ルール: 部屋から出るたびに怪我がないか確認する ( peocess.nextTick() )

Xiaodai は最初に投票ルームに入り、探索を開始し (投票キューを実行)、次にチェック ルーム、タイマー ルーム (ランダム) に入り、探索後に終了し、クローズ コールバックに入り、探索後に io/コールバック ルームに入り、最後に探索を完了し、 を離れます。

シャオダイ氏は、その任務がついに完了したと語った。

参考:

https://zhuanlan.zhihu.com/p/33058983

https://zhuanlan.zhihu.com/p/54882306

 

 

 

おすすめ

転載: blog.csdn.net/qdmoment/article/details/105804253