首頁  >  文章  >  web前端  >  一文聊聊Node.js中的event-loop機制

一文聊聊Node.js中的event-loop機制

青灯夜游
青灯夜游轉載
2021-11-05 09:48:252338瀏覽

本篇文章帶大家一起來了解Node.js中的event-loop(時間循環)機制,希望對大家有幫助!

一文聊聊Node.js中的event-loop機制

今天我們來學習下nodeJs中的event-loop。 event-loop的理解對我來說一直都是一個比較大的難點,希望透過這次的學習把這個難點突破,也希望能透過這篇部落格加深自己對event-loop的理解和印象。

libuv

在學習event-loop之前,先了解下node的libuv。 libuv負責不同作業系統上的不同I/O模型的實現,並且把不同的實作抽象化為能應用與第三方應用程式的API。

一文聊聊Node.js中的event-loop機制

問題

在正式學習event-loop前,先思考一個問題

    setTimeout(() => {
      console.log("timer1");
      Promise.resolve().then(() => {
        console.log("promise1");
      });
    }, 0);

    setTimeout(() => {
      console.log("timer2");
      Promise.resolve().then(() => {
        console.log("promise2");
      });
    }, 0);

這段程式碼在瀏覽器中運行的結果是怎樣的?

一文聊聊Node.js中的event-loop機制

在node中運行的結果又是怎麼樣的呢?

在node8.6之前:

一文聊聊Node.js中的event-loop機制

#node8.6之後:

一文聊聊Node.js中的event-loop機制

為什麼會有這樣的結果,我們稍後會分析!

nodeJs 中的event-loop

首先,來看一張圖:

一文聊聊Node.js中的event-loop機制

在圖中可以看到6個階段,分別是:timers,pending callbacks,idle/prepare,poll,check,close callbacks。

  • timers階段:主要執行setTimeOut,setInterval的回呼

  • pending callbacks階段:執行一些系統呼叫的錯誤,比如說網路通訊的錯誤回呼

  • idle/prepare階段:只在系統內部使用(這個階段我們控制干涉不了)

  • poll階段:取得新的I/O事件,例如取得一個讀取檔案的I/O回呼。 在適合的情況下,nodejs將阻塞在這個階段

  • check階段:執行setImmediate的回呼

  • 例如執行sokect的destory,close事件回呼

每一個階段都遵循一個FIFO(先入先出)的規則來執行任務佇列裡面的任務。 在這六個階段中,我們著重需要關注的是timers,poll,check階段。我們日常開發中絕大部分的非同步任務都是在這三個階段處理的。

timers

我們先來談談timers階段。
timers是事件循環的第一個階段,nodejs會去檢查有沒有已經過期了的timer,如果有,就將它的回調放入佇列中。但是nodejs並不能保證timer在預設事件到了就會立即執行回調,這是因為nodejs對timer的過期檢查不一定可靠,它會受機器上其他運行程序的影響,或者是會遇到當前主線程不空閒的情況。
對於這裡的不確定性,官網上舉了一個例子:
先聲明一個setTimeOut,然後外部讀取一個文件,當讀取文件操作超過定時器的時間,這樣一來讀檔案操作就會把定時器的回調延後,這就是前面說的主執行緒不空閒的情況。

poll

poll階段主要是執行兩件事:

1、處理poll階段的任務佇列 

2、當有了已經逾時的timer執行它的回呼函數

一文聊聊Node.js中的event-loop機制

在上圖中,我們還可以看到:在poll階段執行完poll任務佇列的任務之後,會去檢查有無預設的setImmediate,如果有,則進入check階段,如果沒有,則nodejs將會阻塞在這裡。

這裡我們就會有一個疑問了,如果阻塞在poll階段,那我們設定的timer豈不是執行不了了嗎?
其實當event-loop阻塞在poll階段時,nodejs會有一個檢查機制,它會去檢查timers佇列是否為空,如果不為空,則重新進入timers階段。

check

check階段主要時執行setImmediate的回呼函數。

小总结

event-loop的每个阶段都有一个队列,当event-loop达到某个阶段之后,将执行这个阶段的任务队列,直到队列清空或者达到系统规定的最大回调限制之后,才会进入下一个阶段。当所有阶段都执行完成一次之后,称event-loop完成一个tick。

案例

上面我们说完了event-loop的理论部分,但是光有理论我们也还是不能很清晰的理解event-loop。下面我们就根据几个demo来更加深入的理解下event-loop!

demo1

    const fs=require('fs')
    fs.readFile('test.txt',()=>{
            console.log('readFile')
            setTimeout(()=>{
                    console.log('settimeout');
            },0)
            setImmediate(()=>{
                    console.log('setImmediate')
            })
    })

执行结果:

一文聊聊Node.js中的event-loop機制

可见执行结果跟我们前面的分析时一致的!

demo2

    const fs = require("fs");
    const EventEmitter = require("events").EventEmitter;
    let pos = 0;
    const messenger = new EventEmitter();

    messenger.on("message", function (msg) {
      console.log(++pos + " message:" + msg); //
    });

    console.log(++pos + " first"); //

    process.nextTick(function () {
      console.log(++pos + " nextTick"); //
    });

    messenger.emit("message", "hello!");
    fs.stat(__filename, function () {
      console.log(++pos + " stat"); //
    });

    setTimeout(function () {
      console.log(++pos + " quick timer"); //
    }, 0);
    setTimeout(function () {
      console.log(++pos + " long timer"); //
    }, 30);
    setImmediate(function () {
      console.log(++pos + " immediate"); //
    });

    console.log(++pos + " last"); //

结果:

一文聊聊Node.js中的event-loop機制

了解下浏览器和node的event-loop差异在什么地方

在node 8.6 之前:

浏览器中的微任务队列会在每个宏任务执行完成之后执行,而node中的微任务会在事件循环的各个阶段之间执行,即每个阶段执行完成之后会去执行微任务队列。

在8.6之后:

浏览器和node中微任务的执行是一致的!

所以,在文章开头,我们提出的思考的问题就有了结果。

关于 process.nextTick()和setImmediate

process.nextTick()

语法:process.nextTick(callback,agrs)

执行时机:

这个函数其实是独立于 Event Loop 之外的,它有一个自己的队列,当每个阶段完成后,如果存在 nextTick 队列,就会清空队列中的所有回调函数,并且优先于其他 microtask 执行。递归的调用process.nextTick()会导致I/O starving,官方推荐使用setImmediate()

关于starving现象的说明:

    const fs = require("fs");
    fs.readFile("test.txt", (err, msg) => {
      console.log("readFile");
    });

    let index = 0;

    function handler() {
      if (index >= 30) return;
      index++;
      console.log("nextTick" + index);
      process.nextTick(handler);
    }

    handler();

运行结果:

一文聊聊Node.js中的event-loop機制

可以看到,等到nextTick函数呗执行30次之后,读取文件的回调才被执行!这样的现象被称为 I/O 饥饿

当我们把 process.nextTick 换为 setImmediate

    const fs = require("fs");
    fs.readFile("test.txt", (err, msg) => {
      console.log("readFile");
    });

    let index = 0;

    function handler() {
      if (index >= 30) return;
      index++;
      console.log("nextTick" + index);
      setImmediate(handler);
    }

    handler();

结果:

一文聊聊Node.js中的event-loop機制

造成这两种差异的原因是,嵌套调用的setImmediate的回调被排到了下一次event-loop中去!

event-loop核心思维导图

1一文聊聊Node.js中的event-loop機制

结束语

通过今天的学习,让我对event-loop的理解更深刻了。那么,下次见。好好学习,天天向上!

1一文聊聊Node.js中的event-loop機制

更多编程相关知识,请访问:编程视频!!

以上是一文聊聊Node.js中的event-loop機制的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:juejin.cn。如有侵權,請聯絡admin@php.cn刪除