目錄
什麼是事件循環
事件循環詳解
每個循環階段內容詳解
走进案例解析
1. nextTicksetImmediate
nextTick 递归的危害
2. setImmediate
3. setTimeoutsetImmediate
4. nextTickPromise
5. 最后案例
首頁 web前端 js教程 一文詳解Node.js中的事件循環

一文詳解Node.js中的事件循環

Oct 21, 2022 pm 08:36 PM
nodejs node

這篇文章帶大家深度理解Node中的事件循環,希望對大家有幫助!

一文詳解Node.js中的事件循環

ALL THE TIME,我們寫的的大部分javascript程式碼都是在瀏覽器環境下編譯運行的,因此可能我們對瀏覽器的事件循環機制了解比Node.JS的事件循環更深入一些,但是最近寫開始深入NodeJS學習的時候,發現NodeJS的事件循環機制和瀏覽器端有很大的區別,特此記錄來深入的學習了下,以幫助自己及小夥伴們忘記後查閱及理解。

一文詳解Node.js中的事件循環

什麼是事件循環

#首先我們需要了解最基礎的一些東西,例如這個事件循環,事件循環是指Node.js執行非阻塞I/O操作,儘管==JavaScript是單線程的==,但由於大多數==內核都是多線程==的,Node.js會盡可能將操作裝載到系統核心。因此它們可以處理在背景執行的多個操作。當其中一個操作完成時,核心會告訴Node.js,以便Node.js可以將相應的回調加入到輪詢隊列中以最終執行。 【相關教學推薦:nodejs影片教學

當Node.js啟動時會初始化event loop, 每一個event loop都會包含依下列順序六個循環階段:

   ┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<─────┤  connections, │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘
登入後複製
  • 1. timers 階段: 這個階段執行setTimeout(callback)setInterval(callback) 預定的callback;
  • #2. I/O callbacks 階段: 此階段執行某些系統操作的回調,例如TCP錯誤的類型。例如,如果TCP套接字在嘗試連線時收到 ECONNREFUSED,則某些* nix系統希望等待報告錯誤。這將操作將等待在==I/O回呼階段==執行;
  • 3. idle, prepare 階段: 僅node內部使用;
  • 4. poll 階段: 取得新的I/O事件, 例如操作讀取檔等等,適當的條件下node將阻塞在這裡;
  • 5. check 階段: 執行setImmediate() 設定的callbacks;
  • #6. close callbacks 階段: 例如socket.on('close', callback) 的callback會在這個階段執行;

事件循環詳解

一文詳解Node.js中的事件循環

這個圖是整個Node.js 的運作原理,從左到右,從上到下,Node .js 被分成了四層,分別是應用層V8引擎層Node API層LIBUV層

  • 應用程式層: 即JavaScript 互動層,常見的就是Node.js 的模組,例如 http,fs
  • V8引擎層: 即利用V8 引擎來解析JavaScript語法,進而和下層API 互動
  • NodeAPI層: 為上層模組提供系統調用,一般是由C 語言來實現,和作業系統進行互動。
  • LIBUV層: 是跨平台的底層封裝,實作了 事件循環、檔案操作等,是 Node.js 實作非同步的核心 。

每個循環階段內容詳解

#timers階段 一個timer指定一個下限時間而不是準確時間,在達到這個下限時間後執行回呼。在指定時間過後,timers會盡可能早地執行回調,但係統調度或其它回呼的執行可能會延遲它們。

  • 注意:技術上來說,poll 階段控制 timers 什麼時候執行。

  • 注意:這個下限時間有個範圍:[1, 2147483647],如果設定的時間不在這個範圍,將會被設定為1。

I/O callbacks階段 這個階段執行一些系統運算的回呼。例如TCP錯誤,如一個TCP socket在想要連線時收到ECONNREFUSED, 類別unix系統會等待以報告錯誤,這就會放到 I/O callbacks 階段的佇列執行. 名字會讓人誤解為執行I/O回呼處理程序, 實際上I/O回調會由poll階段處理.

poll阶段 poll 阶段有两个主要功能:(1)执行下限时间已经达到的timers的回调,(2)然后处理 poll 队列里的事件。 当event loop进入 poll 阶段,并且 没有设定的 timers(there are no timers scheduled),会发生下面两件事之一:

  • 如果 poll 队列不空,event loop会遍历队列并同步执行回调,直到队列清空或执行的回调数到达系统上限;

  • 如果 poll 队列为空,则发生以下两件事之一:

    • 如果代码已经被setImmediate()设定了回调, event loop将结束 poll 阶段进入 check 阶段来执行 check 队列(里面的回调 callback)。
    • 如果代码没有被setImmediate()设定回调,event loop将阻塞在该阶段等待回调被加入 poll 队列,并立即执行。
  • 但是,当event loop进入 poll 阶段,并且 有设定的timers,一旦 poll 队列为空(poll 阶段空闲状态): event loop将检查timers,如果有1个或多个timers的下限时间已经到达,event loop将绕回 timers 阶段,并执行 timer 队列。

check阶段 这个阶段允许在 poll 阶段结束后立即执行回调。如果 poll 阶段空闲,并且有被setImmediate()设定的回调,event loop会转到 check 阶段而不是继续等待。

  • setImmediate() 实际上是一个特殊的timer,跑在event loop中一个独立的阶段。它使用libuv的API 来设定在 poll 阶段结束后立即执行回调。

  • 通常上来讲,随着代码执行,event loop终将进入 poll 阶段,在这个阶段等待 incoming connection, request 等等。但是,只要有被setImmediate()设定了回调,一旦 poll 阶段空闲,那么程序将结束 poll 阶段并进入 check 阶段,而不是继续等待 poll 事件们 (poll events)。

close callbacks 阶段 如果一个 socket 或 handle 被突然关掉(比如 socket.destroy()),close事件将在这个阶段被触发,否则将通过process.nextTick()触发

这里呢,我们通过伪代码来说明一下,这个流程:

// 事件循环本身相当于一个死循环,当代码开始执行的时候,事件循环就已经启动了
// 然后顺序调用不同阶段的方法
while(true){
// timer阶段
    timer()
// I/O callbacks阶段
    IO()
// idle阶段
    IDLE()
// poll阶段
    poll()
// check阶段
    check()
// close阶段
    close()
}
// 在一次循环中,当事件循环进入到某一阶段,加入进入到check阶段,突然timer阶段的事件就绪,也会等到当前这次循环结束,再去执行对应的timer阶段的回调函数 
// 下面看这里例子
const fs = require(&#39;fs&#39;)

// timers阶段
const startTime = Date.now();
setTimeout(() => {
    const endTime = Date.now()
    console.log(`timers: ${endTime - startTime}`)
}, 1000)

// poll阶段(等待新的事件出现)
const readFileStart =  Date.now();
fs.readFile(&#39;./Demo.txt&#39;, (err, data) => {
    if (err) throw err
    let endTime = Date.now()
    // 获取文件读取的时间
    console.log(`read time: ${endTime - readFileStart}`)
    // 通过while循环将fs回调强制阻塞5000s
    while(endTime - readFileStart < 5000){
        endTime = Date.now()
    }

})


// check阶段
setImmediate(() => {
    console.log(&#39;check阶段&#39;)
})
/*控制台打印check阶段read time: 9timers: 5008通过上述结果进行分析,1.代码执行到定时器setTimeOut,目前timers阶段对应的事件列表为空,在1000s后才会放入事件2.事件循环进入到poll阶段,开始不断的轮询监听事件3.fs模块异步执行,根据文件大小,可能执行时间长短不同,这里我使用的小文件,事件大概在9s左右4.setImmediate执行,poll阶段暂时未监测到事件,发现有setImmediate函数,跳转到check阶段执行check阶段事件(打印check阶段),第一次时间循环结束,开始下一轮事件循环5.因为时间仍未到定时器截止时间,所以事件循环有一次进入到poll阶段,进行轮询6.读取文件完毕,fs产生了一个事件进入到poll阶段的事件队列,此时事件队列准备执行callback,所以会打印(read time: 9),人工阻塞了5s,虽然此时timer定时器事件已经被添加,但是因为这一阶段的事件循环为完成,所以不会被执行,(如果这里是死循环,那么定时器代码永远无法执行)7.fs回调阻塞5s后,当前事件循环结束,进入到下一轮事件循环,发现timer事件队列有事件,所以开始执行 打印timers: 5008ps:1.将定时器延迟时间改为5ms的时候,小于文件读取时间,那么就会先监听到timers阶段有事件进入,从而进入到timers阶段执行,执行完毕继续进行事件循环check阶段timers: 6read time: 50082.将定时器事件设置为0ms,会在进入到poll阶段的时候发现timers阶段已经有callback,那么会直接执行,然后执行完毕在下一阶段循环,执行check阶段,poll队列的回调函数timers: 2check阶段read time: 7 */
登入後複製

走进案例解析

我们来看一个简单的EventLoop的例子:

const fs = require(&#39;fs&#39;);
let counts = 0;

// 定义一个 wait 方法
function wait (mstime) {
  let date = Date.now();
  while (Date.now() - date < mstime) {
    // do nothing
  }
}

// 读取本地文件 操作IO
function asyncOperation (callback) {
  fs.readFile(__dirname + &#39;/&#39; + __filename, callback);
}

const lastTime = Date.now();

// setTimeout
setTimeout(() => {
  console.log(&#39;timers&#39;, Date.now() - lastTime + &#39;ms&#39;);
}, 0);

// process.nextTick
process.nextTick(() => {
  // 进入event loop
  // timers阶段之前执行
  wait(20);
  asyncOperation(() => {
    console.log(&#39;poll&#39;);
  });  
});

/** * timers 21ms * poll */
登入後複製

这里呢,为了让这个setTimeout优先于fs.readFile 回调, 执行了process.nextTick, 表示在进入timers阶段前, 等待20ms后执行文件读取.

1. nextTicksetImmediate

  • process.nextTick 不属于事件循环的任何一个阶段,它属于该阶段与下阶段之间的过渡, 即本阶段执行结束, 进入下一个阶段前, 所要执行的回调。有给人一种插队的感觉.

  • setImmediate 的回调处于check阶段, 当poll阶段的队列为空, 且check阶段的事件队列存在的时候,切换到check阶段执行,参考nodejs进阶视频讲解:进入学习

nextTick 递归的危害

由于nextTick具有插队的机制,nextTick的递归会让事件循环机制无法进入下一个阶段. 导致I/O处理完成或者定时任务超时后仍然无法执行, 导致了其它事件处理程序处于饥饿状态. 为了防止递归产生的问题, Node.js 提供了一个 process.maxTickDepth (默认 1000)。

const fs = require(&#39;fs&#39;);
let counts = 0;

function wait (mstime) {
  let date = Date.now();
  while (Date.now() - date < mstime) {
    // do nothing
  }
}

function nextTick () {
  process.nextTick(() => {
    wait(20);
    console.log(&#39;nextTick&#39;);
    nextTick();
  });
}

const lastTime = Date.now();

setTimeout(() => {
  console.log(&#39;timers&#39;, Date.now() - lastTime + &#39;ms&#39;);
}, 0);

nextTick();
登入後複製

此时永远无法跳到timer阶段去执行setTimeout里面的回调方法, 因为在进入timers阶段前有不断的nextTick插入执行. 除非执行了1000次到了执行上限,所以上面这个案例会不断地打印出nextTick字符串

2. setImmediate

如果在一个I/O周期内进行调度,setImmediate() 将始终在任何定时器(setTimeout、setInterval)之前执行.

3. setTimeoutsetImmediate

  • setImmediate()被设计在 poll 阶段结束后立即执行回调;
  • setTimeout()被设计在指定下限时间到达后执行回调;

无 I/O 处理情况下:

setTimeout(function timeout () {
  console.log(&#39;timeout&#39;);
},0);

setImmediate(function immediate () {
  console.log(&#39;immediate&#39;);
});
登入後複製

执行结果:

C:\Users\92809\Desktop\node_test>node test.js
timeout
immediate

C:\Users\92809\Desktop\node_test>node test.js
timeout
immediate

C:\Users\92809\Desktop\node_test>node test.js
timeout
immediate

C:\Users\92809\Desktop\node_test>node test.js
immediate
timeout
登入後複製

从结果,我们可以发现,这里打印输出出来的结果,并没有什么固定的先后顺序,偏向于随机,为什么会发生这样的情况呢?

答:首先进入的是timers阶段,如果我们的机器性能一般,那么进入timers阶段,1ms已经过去了 ==(setTimeout(fn, 0)等价于setTimeout(fn, 1))==,那么setTimeout的回调会首先执行。

如果没有到1ms,那么在timers阶段的时候,下限时间没到,setTimeout回调不执行,事件循环来到了poll阶段,这个时候队列为空,于是往下继续,先执行了setImmediate()的回调函数,之后在下一个事件循环再执行setTimemout的回调函数。

问题总结:而我们在==执行启动代码==的时候,进入timers的时间延迟其实是==随机的==,并不是确定的,所以会出现两个函数执行顺序随机的情况。

那我们再来看一段代码:

var fs = require(&#39;fs&#39;)

fs.readFile(__filename, () => {
    setTimeout(() => {
        console.log(&#39;timeout&#39;);
    }, 0);
    setImmediate(() => {
        console.log(&#39;immediate&#39;);
    });
});
登入後複製

打印结果如下:

C:\Users\92809\Desktop\node_test>node test.js
immediate
timeout

C:\Users\92809\Desktop\node_test>node test.js
immediate
timeout

C:\Users\92809\Desktop\node_test>node test.js
immediate
timeout

# ... 省略 n 多次使用 node test.js 命令 ,结果都输出 immediate timeout
登入後複製

这里,为啥和上面的随机timer不一致呢,我们来分析下原因:

原因如下:fs.readFile的回调是在poll阶段执行的,当其回调执行完毕之后,poll队列为空,而setTimeout入了timers的队列,此时有代码 setImmediate(),于是事件循环先进入check阶段执行回调,之后在下一个事件循环再在timers阶段中执行回调。

当然,下面的小案例同理:

setTimeout(() => {
    setImmediate(() => {
        console.log(&#39;setImmediate&#39;);
    });
    setTimeout(() => {
        console.log(&#39;setTimeout&#39;);
    }, 0);
}, 0);
登入後複製

以上的代码在timers阶段执行外部的setTimeout回调后,内层的setTimeoutsetImmediate入队,之后事件循环继续往后面的阶段走,走到poll阶段的时候发现队列为空,此时有代码有setImmedate(),所以直接进入check阶段执行响应回调(==注意这里没有去检测timers队列中是否有成员到达下限事件,因为setImmediate()优先==)。之后在第二个事件循环的timers阶段中再去执行相应的回调。

综上所演示,我们可以总结如下:

  • 如果两者都在主模块中调用,那么执行先后取决于进程性能,也就是你的电脑好撇,当然也就是随机。
  • 如果两者都不在主模块调用(被一个异步操作包裹),那么**setImmediate的回调永远先执行**。

4. nextTickPromise

概念:对于这两个,我们可以把它们理解成一个微任务。也就是说,它其实不属于事件循环的一部分。 那么他们是在什么时候执行呢? 不管在什么地方调用,他们都会在其所处的事件循环最后,事件循环进入下一个循环的阶段前执行。

setTimeout(() => {
    console.log(&#39;timeout0&#39;);
    new Promise((resolve, reject) => { resolve(&#39;resolved&#39;) }).then(res => console.log(res));
    new Promise((resolve, reject) => {
      setTimeout(()=>{
        resolve(&#39;timeout resolved&#39;)
      })
    }).then(res => console.log(res));
    process.nextTick(() => {
        console.log(&#39;nextTick1&#39;);
        process.nextTick(() => {
            console.log(&#39;nextTick2&#39;);
        });
    });
    process.nextTick(() => {
        console.log(&#39;nextTick3&#39;);
    });
    console.log(&#39;sync&#39;);
    setTimeout(() => {
        console.log(&#39;timeout2&#39;);
    }, 0);
}, 0);
登入後複製

控制台打印如下:

C:\Users\92809\Desktop\node_test>node test.js
timeout0
sync
nextTick1
nextTick3
nextTick2
resolved
timeout2
timeout resolved
登入後複製

最总结:timers阶段执行外层setTimeout的回调,遇到同步代码先执行,也就有timeout0sync的输出。遇到process.nextTickPromise后入微任务队列,依次nextTick1nextTick3nextTick2resolved入队后出队输出。之后,在下一个事件循环的timers阶段,执行setTimeout回调输出timeout2以及微任务Promise里面的setTimeout,输出timeout resolved。(这里要说明的是 微任务nextTick优先级要比Promise要高)

5. 最后案例

代码片段1:

setImmediate(function(){
  console.log("setImmediate");
  setImmediate(function(){
    console.log("嵌套setImmediate");
  });
  process.nextTick(function(){
    console.log("nextTick");
  })
});

/*     C:\Users\92809\Desktop\node_test>node test.js    setImmediate    nextTick    嵌套setImmediate*/
登入後複製

解析:

事件循环check阶段执行回调函数输出setImmediate,之后输出nextTick。嵌套的setImmediate在下一个事件循环的check阶段执行回调输出嵌套的setImmediate

代码片段2:

async function async1(){
    console.log(&#39;async1 start&#39;)
    await async2()
    console.log(&#39;async1 end&#39;)
  }
async function async2(){
    console.log(&#39;async2&#39;)
}
console.log(&#39;script start&#39;)
setTimeout(function(){
    console.log(&#39;setTimeout0&#39;) 
},0)  
setTimeout(function(){
    console.log(&#39;setTimeout3&#39;) 
},3)  
setImmediate(() => console.log(&#39;setImmediate&#39;));
process.nextTick(() => console.log(&#39;nextTick&#39;));
async1();
new Promise(function(resolve){
    console.log(&#39;promise1&#39;)
    resolve();
    console.log(&#39;promise2&#39;)
}).then(function(){
    console.log(&#39;promise3&#39;)
})
console.log(&#39;script end&#39;)
登入後複製

打印结果为:

C:\Users\92809\Desktop\node_test>node test.js
script start
async1 start
async2
promise1
promise2
script end
nextTick
promise3
async1 end
setTimeout0
setTimeout3
setImmediate
登入後複製

大家呢,可以先看着代码,默默地在心底走一变代码,然后对比输出的结果,当然最后三位,我个人认为是有点问题的,毕竟在主模块运行,大家的答案,最后三位可能会有偏差;

更多node相关知识,请访问:nodejs 教程

以上是一文詳解Node.js中的事件循環的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
1 個月前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
1 個月前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您聽不到任何人,如何修復音頻
1 個月前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.聊天命令以及如何使用它們
1 個月前 By 尊渡假赌尊渡假赌尊渡假赌

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

nodejs是後端框架嗎 nodejs是後端框架嗎 Apr 21, 2024 am 05:09 AM

Node.js 可作為後端框架使用,因為它提供高效能、可擴展性、跨平台支援、豐富的生態系統和易於開發等功能。

nodejs怎麼連接mysql資料庫 nodejs怎麼連接mysql資料庫 Apr 21, 2024 am 06:13 AM

要連接 MySQL 資料庫,需要遵循以下步驟:安裝 mysql2 驅動程式。使用 mysql2.createConnection() 建立連接對象,其中包含主機位址、連接埠、使用者名稱、密碼和資料庫名稱。使用 connection.query() 執行查詢。最後使用 connection.end() 結束連線。

nodejs安裝目錄裡的npm與npm.cmd檔有什麼差別 nodejs安裝目錄裡的npm與npm.cmd檔有什麼差別 Apr 21, 2024 am 05:18 AM

Node.js 安裝目錄中有兩個與 npm 相關的文件:npm 和 npm.cmd,區別如下:擴展名不同:npm 是可執行文件,npm.cmd 是命令視窗快捷方式。 Windows 使用者:npm.cmd 可以在命令提示字元中使用,npm 只能從命令列執行。相容性:npm.cmd 特定於 Windows 系統,npm 跨平台可用。使用建議:Windows 使用者使用 npm.cmd,其他作業系統使用 npm。

nodejs中的全域變數有哪些 nodejs中的全域變數有哪些 Apr 21, 2024 am 04:54 AM

Node.js 中存在以下全域變數:全域物件:global核心模組:process、console、require執行階段環境變數:__dirname、__filename、__line、__column常數:undefined、null、NaN、Infinity、-Infinity

nodejs和java的差別大嗎 nodejs和java的差別大嗎 Apr 21, 2024 am 06:12 AM

Node.js 和 Java 的主要差異在於設計和特性:事件驅動與執行緒驅動:Node.js 基於事件驅動,Java 基於執行緒驅動。單執行緒與多執行緒:Node.js 使用單執行緒事件循環,Java 使用多執行緒架構。執行時間環境:Node.js 在 V8 JavaScript 引擎上運行,而 Java 在 JVM 上運行。語法:Node.js 使用 JavaScript 語法,而 Java 使用 Java 語法。用途:Node.js 適用於 I/O 密集型任務,而 Java 適用於大型企業應用程式。

Pi Node教學:什麼是Pi節點?如何安裝和設定Pi Node? Pi Node教學:什麼是Pi節點?如何安裝和設定Pi Node? Mar 05, 2025 pm 05:57 PM

PiNetwork節點詳解及安裝指南本文將詳細介紹PiNetwork生態系統中的關鍵角色——Pi節點,並提供安裝和配置的完整步驟。 Pi節點在PiNetwork區塊鏈測試網推出後,成為眾多先鋒積極參與測試的重要環節,為即將到來的主網發布做準備。如果您還不了解PiNetwork,請參考Pi幣是什麼?上市價格多少? Pi用途、挖礦及安全性分析。什麼是PiNetwork? PiNetwork項目始於2019年,擁有其專屬加密貨幣Pi幣。該項目旨在創建一個人人可參與

nodejs是後端開發語言嗎 nodejs是後端開發語言嗎 Apr 21, 2024 am 05:09 AM

是的,Node.js 是一種後端開發語言。它用於後端開發,包括處理伺服器端業務邏輯、管理資料庫連接和提供 API。

nodejs專案怎麼部署到伺服器 nodejs專案怎麼部署到伺服器 Apr 21, 2024 am 04:40 AM

Node.js 專案的伺服器部署步驟:準備部署環境:取得伺服器存取權限、安裝 Node.js、設定 Git 儲存庫。建置應用程式:使用 npm run build 產生可部署程式碼和相依性。上傳程式碼到伺服器:透過 Git 或檔案傳輸協定。安裝依賴項:SSH 登入伺服器並使用 npm install 安裝應用程式相依性。啟動應用程式:使用 node index.js 等命令啟動應用程序,或使用 pm2 等進程管理器。設定反向代理(可選):使用 Nginx 或 Apache 等反向代理路由流量到應用程式

See all articles