ホームページ  >  記事  >  ウェブフロントエンド  >  ブラウザとノードのイベント ループの違いの概要 (イベント ループ)

ブラウザとノードのイベント ループの違いの概要 (イベント ループ)

不言
不言転載
2019-01-15 09:35:122450ブラウズ

この記事は、ブラウザと Node のイベント ループ (イベント ループ) の違いをまとめたものです。必要な方は参考にしてください。

この記事では、JS での非同期実装の原理を紹介し、実際にはブラウザーとノードでイベント ループが異なることを理解します。

1. スレッドとプロセス

1. 概念

JS は単一のスレッドで実行されるとよく​​言われます。これは、プロセス内にメイン スレッドが 1 つしかないことを意味します。では、スレッドとは正確には何でしょうか?プロセスとは何ですか?

公式声明は次のとおりです: プロセスは CPU リソース割り当ての最小単位であり、スレッドは CPU スケジューリングの最小単位です。これら 2 つの文は理解するのが簡単ではありません。まず写真を見てみましょう:

ブラウザとノードのイベント ループの違いの概要 (イベント ループ)

    そのプロセスは、次のような工場です。独立した専用の工場リソースを用意します。
  • スレッドは、図でいう労働者のようなものです。工場では複数の労働者が協力して作業します。工場と労働者の関係は 1 対 n です。つまり、
  • プロセスは 1 つ以上のスレッドで構成され、スレッドはプロセス内のコードの異なる実行ルートです

    ;

  • ファクトリ スペースはワーカーによって共有されますこれは、プロセスのメモリ空間が共有されており、各スレッドがこれらの共有メモリを使用できることを象徴しています。
  • #複数の工場が独立して存在します。

  • 2. マルチプロセスとマルチスレッド

マルチプロセス: 同時に 2 つ以上のプロセスが許可されている場合同じコンピュータ システムで、3 つ以上のプロセスが実行されています。複数のプロセスのメリットは明らかです。たとえば、音楽を聴きながらエディターを開いてコードを入力することができ、エディターと音楽視聴ソフトウェアのプロセスはまったく干渉しません。

  • マルチスレッド: プログラムには複数の実行ストリームが含まれています。つまり、1 つのプログラム内で複数の異なるスレッドを同時に実行して、異なるタスクを実行できます。つまり、1 つのプログラムで以下のことが許可されます。並列実行する複数のスレッドを作成して、それぞれのタスクを完了します。

  • Chrome ブラウザを例に挙げると、タブ ページを開くと、プロセス内に複数のスレッドが存在することがあります (詳細は後述します)。たとえば、レンダリング スレッド、JS エンジン スレッド、HTTP リクエスト スレッドなどです。リクエストを開始すると、実際にはスレッドが作成され、リクエストが終了すると、スレッドが破棄される可能性があります。

    2. ブラウザ カーネル
簡単に言うと、ブラウザ カーネルは、ページ コンテンツを取得し、情報を整理し (CSS を適用し)、視覚的な画像結果の最終出力を計算して結合します。これも通常は必要となります。レンダリングエンジン。

ブラウザ カーネルはマルチスレッドであり、カーネルの制御下で、各スレッドは相互に連携して同期を維持します。通常、ブラウザは次の常駐スレッドで構成されます。

GUI レンダリング スレッド

JavaScript エンジン スレッド

  • 時間指定トリガー スレッド

  • イベント トリガースレッド

  • 非同期 http リクエスト スレッド

  • 1. GUI レンダリング スレッド

  • ページを主に担当します。 HTML、CSSのレンダリングと解析、DOMツリーの構築、レイアウトと描画など。

このスレッドは、インターフェイスを再描画する必要がある場合、または何らかの操作によってリフローが発生した場合に実行されます。

  • このスレッドは JS エンジン スレッドと相互に排他的です。JS エンジン スレッドが実行されると、タスク キューがアイドル状態になると、JS エンジンは GUI を実行します。レンダリング。

  • 2. JS エンジン スレッド

  • このスレッドは、もちろん JavaScript スクリプトの処理とコードの実行を主に担当します。

は、実行準備ができているイベントの実行も主に担当します。つまり、タイマーのカウントが終了したとき、または非同期リクエストが成功して正しく返されたときに、タスクに入ります。順番にキューに入れて、JS エンジン スレッドの実行を待ちます。

  • もちろん、このスレッドは GUI レンダリング スレッドと相互排他的です。JS エンジン スレッドが JavaScript スクリプトを長時間実行すると、ページのレンダリングがブロックされます。

  • 3. タイマー トリガー スレッド

  • #setTimeout、setInterval などの非同期タイマーなどの関数の実行を担当するスレッド。

メインスレッドがコードを順番に実行する際にタイマーが発生すると、カウントが完了するとイベントをトリガーするスレッドにタイマーを渡して処理します。カウントが完了したらイベントを追加し、タスク キューの最後に移動して、JS エンジン スレッドが実行されるのを待ちます。

  • 4. イベント トリガー スレッド

  • は主に、準備されたイベントを実行のために JS エンジン スレッドに渡す役割を果たします。

たとえば、setTimeout タイマーのカウントが終了すると、ajax は非同期リクエストが成功するのを待ってコールバック関数をトリガーするか、ユーザーがクリック イベントをトリガーすると、スレッドがイベントをタスクキューに順番に送信する準備ができたら、最後に JS エンジン スレッドの実行を待ちます。

    5. 非同期 http リクエスト スレッド
  • Promise、axios、ajax などの非同期リクエストなどの関数の実行を担当するスレッド。
  • メイン スレッドがコードを順番に実行し、非同期リクエストが発生すると、ステータス コードの変更が監視され、コールバック関数がある場合、関数はスレッドに渡されて処理されます。を実行すると、イベント トリガー スレッドがコールバック関数を追加し、タスク キューの最後に移動して、JS エンジン スレッドが実行されるのを待ちます。

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

    1. マイクロタスクとマクロタスク

    イベントには 2 種類の非同期キューがあります。ループ : マクロ (マクロタスク) キューとマイクロ (マイクロタスク) キュー。 マクロ タスク キューは複数存在できますが、マイクロ タスク キューは 1 つだけです。

    • 一般的なマクロタスク: setTimeout、setInterval、setImmediate、スクリプト (コード全体)、I/O 操作、UI レンダリングなど。

    • 一般的なマイクロタスク: process.nextTick、新しい Promise().then(callback)、MutationObserver(html5 新機能) など。

    #2. イベント ループ プロセスの分析

    完全なイベント ループ プロセスは次の段階に要約できます:

    ブラウザとノードのイベント ループの違いの概要 (イベント ループ)

    • 実行スタックは最初は空です。

      実行スタックは、先入れ、後入れに続いて関数呼び出しを格納するスタック構造と考えることができます。アウト原則。マイクロ キューは空で、マクロ キューにはスクリプト (コード全体) が 1 つだけあります。

    • グローバル コンテキスト (スクリプト タグ) が実行スタックにプッシュされ、コードの実行が同期されます。実行過程で同期タスクか非同期タスクかを判断し、いくつかのインターフェースを呼び出すことで新しいマクロタスクとマイクロタスクを生成し、それぞれのタスクキューにプッシュします。同期コードが実行された後、スクリプトはマクロ キューから削除されます。このプロセスは基本的に、キュー内のマクロ タスクの実行およびデキュー プロセスです。

    • 前のステップではマクロタスクをデキューしましたが、このステップではマイクロタスクを扱います。ただし、マクロタスクがキューから取り出される場合、タスクは

      ずつ実行され、マイクロタスクがキューから取り出される場合、タスクは 一度に 1 チームずつ実行されることに注意してください。 #。したがって、マイクロキューを処理するときは、キュー内のタスクを 1 つずつ実行し、キューが空になるまでデキューします。

    • レンダリング操作を実行し、インターフェイスを更新します。
    • Web ワーカー タスクがあるかどうかを確認し、存在する場合は、処理を実行します。
    • 上記のプロセスは、両方のキューがクリアされるまでループで繰り返されます。
    • 要約すると、各サイクルは次のようなプロセスです。これ:

    ブラウザとノードのイベント ループの違いの概要 (イベント ループ)マクロ タスクが実行されると、マイクロ タスク キューがあるかどうかが確認されます。存在する場合は、マイクロタスク キュー内のすべてのタスクが最初に実行されます。そうでない場合は、マクロタスクの実行中に、マイクロタスクが見つかった場合、そのタスクがマイクロタスク キューに追加されます。振り向く。スタックが空になったら、マイクロタスク キュー内のタスクを再度読み取ります。以下同様です。

    次に、上記のプロセスを紹介する例を見てみましょう。

    Promise.resolve().then(()=>{
      console.log('Promise1')
      setTimeout(()=>{
        console.log('setTimeout2')
      },0)
    })
    setTimeout(()=>{
      console.log('setTimeout1')
      Promise.resolve().then(()=>{
        console.log('Promise2')
      })
    },0)
    最終的な出力結果は、Promise1、setTimeout1、Promise2、setTimeout2

    です。

    スタックの同期タスク (これはマクロ タスクです) の実行が完了した後、上記の質問にあるマイクロタスク キューがあるかどうかを確認し (1 つだけあります)、すべてのタスクを実行します。マイクロタスクキュー内のタスクを実行して Promise1 を出力すると同時に、マクロタスク setTimeout2
    • を生成し、マクロタスクキューを確認します。マクロタスク setTimeout1 の前に、マクロタスクを実行します。最初に setTimeout1 を実行し、setTimeout1
    • ## を出力します。 #マクロタスク setTimeout1 を実行すると、マイクロタスク Promise2 が生成され、マイクロタスク キューに配置されます。次に、最初にマイクロタスク キュー内のすべてのタスクをクリアして、Promise2

      # を出力します。
    • ##マイクロタスクのクリア後 すべてのタスクがキューに入ったら、今度は別のタスクを取得するためにマクロ タスク キューに移動します。 4. ノードのイベント ループ

      1 ノードの概要
    • #ノードのイベント ループはブラウザのイベント ループとはまったく異なります。 Node.js は、JS 解析エンジンとして V8 を使用し、I/O 処理に独自に設計された libuv を使用します。libuv は、さまざまなオペレーティング システムの基礎となる機能の一部をカプセル化し、統合された API を提供するイベント駆動型のクロスプラットフォーム抽象化レイヤーです。の外の世界には、イベント ループのメカニズムも実装されています (詳細は後述します)。

    Node.js の動作メカニズムは次のとおりです。

    V8 エンジンは JavaScript スクリプトを解析します。 。

    ブラウザとノードのイベント ループの違いの概要 (イベント ループ)解析されたコードは、Node API を呼び出します。

    libuv ライブラリは、ノード API の実行を担当します。異なるタスクを異なるスレッドに割り当ててイベント ループを形成し、タスクの実行結果を非同期で V8 エンジンに返します。
    • V8 引擎再将结果返回给用户。

    2. 六个阶段

    其中 libuv 引擎中的事件循环分为 6 个阶段,它们会按照顺序反复运行。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段。

    ブラウザとノードのイベント ループの違いの概要 (イベント ループ)

    从上图中,大致看出 node 中的事件循环的顺序:

    外部输入数据-->轮询阶段(poll)-->检查阶段(check)-->关闭事件回调阶段(close callback)-->定时器检测阶段(timer)-->I/O 事件回调阶段(I/O callbacks)-->闲置阶段(idle, prepare)-->轮询阶段(按照该顺序反复运行)...

    • timers 阶段:这个阶段执行 timer(setTimeout、setInterval)的回调

    • I/O callbacks 阶段:处理一些上一轮循环中的少数未执行的 I/O 回调

    • idle, prepare 阶段:仅 node 内部使用

    • poll 阶段:获取新的 I/O 事件, 适当的条件下 node 将阻塞在这里

    • check 阶段:执行 setImmediate() 的回调

    • close callbacks 阶段:执行 socket 的 close 事件回调

    注意:上面六个阶段都不包括 process.nextTick()(下文会介绍)

    接下去我们详细介绍timerspollcheck这 3 个阶段,因为日常开发中的绝大部分异步任务都是在这 3 个阶段处理的。

    (1) timer

    timers 阶段会执行 setTimeout 和 setInterval 回调,并且是由 poll 阶段控制的。
    同样,在 Node 中定时器指定的时间也不是准确时间,只能是尽快执行

    (2) poll

    poll 是一个至关重要的阶段,这一阶段中,系统会做两件事情

    • 回到 timer 阶段执行回调

    • 执行 I/O 回调

    并且在进入该阶段时如果没有设定了 timer 的话,会发生以下两件事情

    • 如果 poll 队列不为空,会遍历回调队列并同步执行,直到队列为空或者达到系统限制

    • 如果 poll 队列为空时,会有两件事发生

      • 如果有 setImmediate 回调需要执行,poll 阶段会停止并且进入到 check 阶段执行回调

      • 如果没有 setImmediate 回调需要执行,会等待回调被加入到队列中并立即执行回调,这里同样会有个超时时间设置防止一直等待下去

    当然设定了 timer 的话且 poll 队列为空,则会判断是否有 timer 超时,如果有的话会回到 timer 阶段执行回调。

    (3) check 阶段

    setImmediate()的回调会被加入 check 队列中,从 event loop 的阶段图可以知道,check 阶段的执行顺序在 poll 阶段之后。

    我们先来看个例子:

    console.log('start')
    setTimeout(() => {
      console.log('timer1')
      Promise.resolve().then(function() {
        console.log('promise1')
      })
    }, 0)
    setTimeout(() => {
      console.log('timer2')
      Promise.resolve().then(function() {
        console.log('promise2')
      })
    }, 0)
    Promise.resolve().then(function() {
      console.log('promise3')
    })
    console.log('end')
    //start=>end=>promise3=>timer1=>timer2=>promise1=>promise2
    • 一开始执行栈的同步任务(这属于宏任务)执行完毕后(依次打印出 start end,并将 2 个 timer 依次放入 timer 队列),会先去执行微任务(这点跟浏览器端的一样),所以打印出 promise3

    • 然后进入 timers 阶段,执行 timer1 的回调函数,打印 timer1,并将 promise.then 回调放入 microtask 队列,同样的步骤执行 timer2,打印 timer2;这点跟浏览器端相差比较大,timers 阶段有几个 setTimeout/setInterval 都会依次执行,并不像浏览器端,每执行一个宏任务后就去执行一个微任务(关于 Node 与浏览器的 Event Loop 差异,下文还会详细介绍)。

    3. 注意点

    (1) setTimeout 和 setImmediate

    二者非常相似,区别主要在于调用时机不同。

    • setImmediate 设计在 poll 阶段完成时执行,即 check 阶段;

    • setTimeout 设计在 poll 阶段为空闲时,且设定时间到达后执行,但它在 timer 阶段执行

    setTimeout(function timeout () {
      console.log('timeout');
    },0);
    setImmediate(function immediate () {
      console.log('immediate');
    });
    • 对于以上代码来说,setTimeout 可能执行在前,也可能执行在后。

    • 首先 setTimeout(fn, 0) === setTimeout(fn, 1),这是由源码决定的
      进入事件循环也是需要成本的,如果在准备时候花费了大于 1ms 的时间,那么在 timer 阶段就会直接执行 setTimeout 回调

    • 如果准备时间花费小于 1ms,那么就是 setImmediate 回调先执行了

    但当二者在异步 i/o callback 内部调用时,总是先执行 setImmediate,再执行 setTimeout

    const fs = require('fs')
    fs.readFile(__filename, () => {
        setTimeout(() => {
            console.log('timeout');
        }, 0)
        setImmediate(() => {
            console.log('immediate')
        })
    })
    // immediate
    // timeout

    在上述代码中,setImmediate 永远先执行。因为两个代码写在 IO 回调中,IO 回调是在 poll 阶段执行,当回调执行完毕后队列为空,发现存在 setImmediate 回调,所以就直接跳转到 check 阶段去执行回调了。

    (2) process.nextTick

    这个函数其实是独立于 Event Loop 之外的,它有一个自己的队列,当每个阶段完成后,如果存在 nextTick 队列,就会清空队列中的所有回调函数,并且优先于其他 microtask 执行。

    setTimeout(() => {
     console.log('timer1')
     Promise.resolve().then(function() {
       console.log('promise1')
     })
    }, 0)
    process.nextTick(() => {
     console.log('nextTick')
     process.nextTick(() => {
       console.log('nextTick')
       process.nextTick(() => {
         console.log('nextTick')
         process.nextTick(() => {
           console.log('nextTick')
         })
       })
     })
    })
    // nextTick=>nextTick=>nextTick=>nextTick=>timer1=>promise1

    五、Node 与浏览器的 Event Loop 差异

    浏览器环境下,microtask 的任务队列是每个 macrotask 执行完之后执行。而在 Node.js 中,microtask 会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行 microtask 队列的任务

    ブラウザとノードのイベント ループの違いの概要 (イベント ループ)

    接下我们通过一个例子来说明两者区别:

    setTimeout(()=>{
        console.log('timer1')
        Promise.resolve().then(function() {
            console.log('promise1')
        })
    }, 0)
    setTimeout(()=>{
        console.log('timer2')
        Promise.resolve().then(function() {
            console.log('promise2')
        })
    }, 0)

    浏览器端运行结果:timer1=>promise1=>timer2=>promise2

    浏览器端的处理过程如下:

    ブラウザとノードのイベント ループの違いの概要 (イベント ループ)

    Node 端运行结果:timer1=>timer2=>promise1=>promise2

    • 全局脚本(main())执行,将 2 个 timer 依次放入 timer 队列,main()执行完毕,调用栈空闲,任务队列开始执行;

    • 首先进入 timers 阶段,执行 timer1 的回调函数,打印 timer1,并将 promise1.then 回调放入 microtask 队列,同样的步骤执行 timer2,打印 timer2;

    • 至此,timer 阶段执行结束,event loop 进入下一个阶段之前,执行 microtask 队列的所有任务,依次打印 promise1、promise2

    Node 端的处理过程如下:

    ブラウザとノードのイベント ループの違いの概要 (イベント ループ)

    六、总结

    浏览器和 Node 环境下,microtask 任务队列的执行时机不同

    • Node 端,microtask 在事件循环的各个阶段之间执行

    • 浏览器端,microtask 在事件循环的 macrotask 执行完之后执行

    以上がブラウザとノードのイベント ループの違いの概要 (イベント ループ)の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

    声明:
    この記事はsegmentfault.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。