搜尋
首頁web前端js教程Node.js 事件循環
Node.js 事件循環Nov 18, 2016 pm 12:50 PM
node.js

什麼是事件循環(Event Loop)

 

事件循環能讓Node.js 執行非阻塞I/O 操作-- 儘管JavaScript事實上是單線程的-- 透過在可能的情況下把操作交給操作系統核心來實現。

 

由於大多數現代系統核心是多執行緒的,核心可以處理後台執行的多個操作。當其中一個操作完成的時候,核心告訴 Node.js,相應的回調就被加入到輪詢隊列(poll queue)並最終執行。本主題接著會說明更多相關細節。

 

 

事件循環

 

Node.js 開始的時候會初始化事件循環,處理目標腳本,腳本可能會進行異步API、定時任務或process.nextTickcess.nextTickcess.nextTickcess.nextTickcess。

 

下面的表格簡單描述了事件循環的操作順序。

Node.js 事件循環

註:每個方框代表事件循環中的一個階段。

 

每個階段都有一個需要執行的回呼函數的先入先出(FIFO)隊列。同時,每個階段都是特殊的,基本上,當事件循環進行到某個階段時,會執行該階段特有的操作,然後執行該階段隊列中的回調,直到隊列空了或達到了執行次數限制。這時候,事件循環會進入下一個階段,循環往復。

 

由於這些操作可能產生更多的計劃任務操作,並且輪詢階段處理的新事件會被加入到核心的隊列,輪詢事件被處理的時候會有新的輪詢事件加入。於是,長時回調任務會導致輪詢階段的時間超過了定時器的閾值。 詳情請見 定時器(timers)和輪詢(poll)部分。

 

註:Windows 和 Unix/Linux 的實現有輕微的矛盾之處,但不影響剛才的描述。 最重要的部分都有了。其實有七、八個階段,但我們關注的 -- Node.js 實際使用的 -- 就是上面這些。

 

 

階段總覽(Phases Overview)

 

計時器(timers):本階段執行setTimeout() 和setInterval() 計劃的回調;回調, 由定時器和setImmediate()計劃的回調;

空閒,預備(idle,prepare):只內部使用;

輪詢(poll): 獲取新的I/O 事件;nodejs這時會適當進行進行阻塞;

檢查(check): 調用setImmediate() 的回調;

close callbacks: 例如socket.on('close', ... );

 

在事件循環運行之間,Node.js 檢查是否有正在等待的非同步I/O 或定時器,如果沒有就清除並結束。

 

 

階段細部

 

定時器(timers)

 

定時器的用途是讓指定的時間點回準的閾值。定時器的回調會在製定的時間過後儘快得到執行,然而,作業系統的計畫或其他回呼的執行可能會延遲該回呼的執行。

 

註:從技術上來看,輪詢階段控制了定時器的執行時機。

 

例如,你設定了在100ms後執行某個操作,然後腳本開始執行一個需要95ms的文件讀取操作:

var fs = require('fs');

function someAsyncOperation (callback) {
  // Assume this takes 95ms to complete
  fs.readFile('/path/to/file', callback);
}

var timeoutScheduled = Date.now();

setTimeout(function () {

  var delay = Date.now() - timeoutScheduled;

  console.log(delay + "ms have passed since I was scheduled");
}, 100);


// do someAsyncOperation which takes 95 ms to completesomeAsyncOperation(function () {

  var startCallback = Date.now();

  // do something that will take 10ms...
  while (Date.now() - startCallback < 10) {
    ; // do nothing
  }

});

當事件循環進入輪詢階段時,隊列是空的(fs. readFile()還沒完成),因此時間會繼續流逝知道最快的定時器需要執行。過了95ms後,fs.readFile() 讀完檔案了,它的回呼被加入到輪詢隊列,這個回呼需要執行10ms。等到這個回呼執行完,佇列中沒有回調了,這時事件循環看到了最近到時的定時器,然後回到定時器階段(timers phase)來執行先前的定時器回調。

在這個例子中,從定義定時器到回調執行中間過了105ms。

 

註:為了防止輪詢階段持續時間太長,libuv 會根據作業系統的不同設定一個輪詢的上限。

 

 

I/O callbacks

 

這個階段執行一些諸如TCP錯誤之類的系統操作的回調。例如,如果一個TCP socket 在嘗試連線時收到了 ECONNREFUSED錯誤,某些 *nix 系統會等著報告這個錯誤。這個就會被排到本階段的隊列中。

 

 

輪詢(poll)

 

輪詢階段有兩個主要功能:

1,執行已經到時的定時器腳本,然後

隊列中

2,處理

 

當事件循環進入到輪詢階段卻沒有發現定時器時:

如果輪詢隊列非空,事件循環會迭代回調隊列並同步執行回調,直到隊列空了或者達到了上限(前文說過的根據作業系統的不同而設定的上限)。

如果轮询队列是空的:


如果有setImmediate()定义了回调,那么事件循环会终止轮询阶段并进入检查阶段去执行定时器回调;

如果没有setImmediate(),事件回调会等待回调被加入队列并立即执行。

 

一旦轮询队列空了,事件循环会查找已经到时的定时器。如果找到了,事件循环就回到定时器阶段去执行回调。

 

 

检查(check)

 

这个阶段允许回调函数在轮询阶段完成后立即执行。如果轮询阶段空闲了,并且有回调已经被 setImmediate() 加入队列,事件循环会进入检查阶段而不是在轮询阶段等待。

 

setImmediate() 是个特殊的定时器,在事件循环中一个单独的阶段运行。它使用libuv的API 来使得回调函数在轮询阶段完成后执行。

 

基本上,随着代码的执行,事件循环会最终进入到等待状态的轮询阶段,可能是等待一个连接、请求等。然而,如果有一个setImmediate() 设置了一个回调并且轮询阶段空闲了,那么事件循环会进入到检查阶段而不是等待轮询事件。   ---- 这车轱辘话说来说去的

 

 

关闭事件的回调(close callbacks)

 

如果一个 socket 或句柄(handle)被突然关闭(is closed abruptly),例如 socket.destroy(), 'close' 事件会被发出到这个阶段。否则这种事件会通过 process.nextTick() 被发出。

 

 

setImmediate() vs setTimeout()

 

这两个很相似,但调用时机会的不同会导致它们不同的表现。

 

setImmediate() 被设计成一旦轮询阶段完成就执行回调函数;

setTimeout() 规划了在某个时间值过后执行回调函数;

 

这两个执行的顺序会因为它们被调用时的上下文而有所不同。如果都是在主模块调用,那么它们会受到进程性能的影响(运行在本机的其他程序会影响它们)。

 

例如,如果我们在非 I/O 循环中运行下面的脚本(即在主模块中),他俩的顺序是不固定的,因为会受到进程性能的影响:

// timeout_vs_immediate.jssetTimeout(function timeout () {
  console.log(&#39;timeout&#39;);
},0);

setImmediate(function immediate () {
  console.log(&#39;immediate&#39;);
});

$ node timeout_vs_immediate.js

timeout

immediate


$ node timeout_vs_immediate.js

immediate

timeout

但是如果把它们放进 I/O 循环中,setImmediate() 的回调总是先执行:

// timeout_vs_immediate.jsvar fs = require(&#39;fs&#39;)

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

$ node timeout_vs_immediate.js

immediate

timeout


$ node timeout_vs_immediate.js

immediate

timeout

 

setImmediate() 比 setTimeout() 优势的地方是 setImmediate() 在 I/O 循环中总是先于任何定时器,不管已经定义了多少定时器。

 

 

process.nextTick()

 

理解 process.nextTick()

 

你可能已经注意到了 process.nextTick() 没有在上面那个表格里出现,虽然它确实是一个异步API。这是因为它技术上不属于事件循环。然而,nextTickQueue 会在当前操作结束后被处理,不管是在事件循环的哪个阶段。

 

回头看看之前那个表格,你在某个阶段的任何时候调用它,它的所有回调函数都会在事件循环继续进行之前得到处理。有时候这会导致比较糟糕的情况,因为它允许你用递归调用的方式去“阻塞” I/O,这会让事件循环无法进入到轮询阶段。

 

为什么要允许这样

 

部分是因为 Node.js 的设计哲学:API 应该总是异步的,即使本不需要是异步。

 

blablabla,后面几段看的我有点尴尬+晕。既尴尬又晕是觉得这几段说的有点啰嗦,而且举的例子不合适。例子要么是同步的,不是异步的。要么是例子里的写法完全可以避免,比如应该先添加 'connect' 事件监听再进行 .connect() 操作;又或者变量声明最好放在变量使用之前,可以避免变量的提前声明和当时赋值的麻烦。

 

难道是我没理解里面的秘辛?

 

 

process.nextTick() vs setTimeout()

 

这两个函数有些相似但是名字让人困惑:

process.netxtTick() 在事件循环的当前阶段立即生效;

setImmediate() 生效是在接下来的迭代或者事件循环的下一次tick;

 

本质上,它们的名字应该互换一下。process.nextTick() 比 setImmediate() 更“立刻”执行,但这是个历史问题没法改变。如果改了,npm上大堆的包就要挂了。

 

我们推荐开发者在所有情况下都使用 setImmediate() 因为它更显而易见(reason about),另外兼容性也更广,例如浏览器端。

 

为什么使用 process.nextTick() 

 

有两大原因:

 

允许用户处理错误,清理不需要的资源,或许在事件循环结束前再次尝试发送请求;

必须让回调函数在调用栈已经清除(unwound)后并且事件循环继续下去之前执行;

 

下面的两个例子都是类似的,即在 line1 派发事件,却在 line2 才添加监听,因此监听的回调是不可能被执行到的。

于是可以用 process.nextTick() 使得当前调用栈先执行完毕,也即先执行 line2 注册事件监听,然后在 nextTick 派发事件。

const EventEmitter = require(&#39;events&#39;);
const util = require(&#39;util&#39;);

function MyEmitter() {
  EventEmitter.call(this);

  // use nextTick to emit the event once a handler is assigned
  process.nextTick(function () {
    this.emit(&#39;event&#39;);
  }.bind(this));
}
util.inherits(MyEmitter, EventEmitter);

const myEmitter = new MyEmitter();
myEmitter.on(&#39;event&#39;, function() {
  console.log(&#39;an event occurred!&#39;);
});

翻译总结:

 

这篇文章写的不太简练,也可能为了有更多的受众吧,我感觉车轱辘话比较多,一个意思要说好几遍。

 

从编程应用的角度简单来说:

 

Node.js 中的事件循环大概有七八个阶段,每个阶段都有自己的队列(queue),需要等本阶段的队列处理完成后才进入其他阶段。阶段之间会互相转换,循环顺序并不是完全固定的 ,因为很多阶段是由外部的事件触发的。

 

其中比较重要的是三个:

 

定时器阶段 timers:
定时器阶段执行定时器任务(setTimeOut(), setInterval())。

轮询阶段 poll:

          轮询阶段由 I/O 事件触发,例如 'connect','data' 等。这是比较重/重要的阶段,因为大部分程序功能就是为了 I/O 数据。

          本阶段会处理定时器任务和 poll 队列中的任务,具体逻辑:


如果有 setImmediate(),终止轮询阶段并进入检查阶段去执行;

如果没有 setImmediate(),那么就查看有没有到期的定时器,有的话就回到定时器阶段执行回调函数;

处理到期的定时器任务,然后

处理队列任务,直到队列空了或者达到上限

如果队列任务没了:


检查阶段 check:

          当轮询阶段空闲并且已经有 setImmediate() 的时候,会进入检查阶段并执行。

 

比较次要但也列在表格中的两个:

 

I/O 阶段:

          本阶段处理 I/O 异常错误;

'close'事件回调:

          本阶段处理各种 'close' 事件回调;

 

关于 setTimeout(), setImmediate(), process.nextTick():

 

setTimeout()           在某个时间值过后尽快执行回调函数;

setImmediate()       一旦轮询阶段完成就执行回调函数;

process.nextTick()   在当前调用栈结束后就立即处理,这时也必然是“事件循环继续进行之前” ;

 

优先级顺序从高到低: process.nextTick() > setImmediate() > setTimeout()

注:这里只是多数情况下,即轮询阶段(I/O 回调中)。比如之前比较 setImmediate() 和 setTimeout() 的时候就区分了所处阶段/上下文。

 

 

另:

 

关于调用栈,事件循环还可以参考这篇文章:

https://blog.risingstack.com/node-js-at-scale-understanding-node-js-event-loop/

 

这篇文章里对事件任务区分了大任务(macro task) 、小任务(micro task),每个事件循环只处理一个大任务 ,但会处理完所有小任务。

这一点和前面的文章说的不同。

examples of microtasks:

process.nextTick

promises

Object.observe

examples of macrotasks:

setTimeout

setInterval

setImmediate

I/O


陳述
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
Vercel是什么?怎么部署Node服务?Vercel是什么?怎么部署Node服务?May 07, 2022 pm 09:34 PM

Vercel是什么?本篇文章带大家了解一下Vercel,并介绍一下在Vercel中部署 Node 服务的方法,希望对大家有所帮助!

node.js gm是什么node.js gm是什么Jul 12, 2022 pm 06:28 PM

gm是基于node.js的图片处理插件,它封装了图片处理工具GraphicsMagick(GM)和ImageMagick(IM),可使用spawn的方式调用。gm插件不是node默认安装的,需执行“npm install gm -S”进行安装才可使用。

怎么使用pkg将Node.js项目打包为可执行文件?怎么使用pkg将Node.js项目打包为可执行文件?Jul 26, 2022 pm 07:33 PM

如何用pkg打包nodejs可执行文件?下面本篇文章给大家介绍一下使用pkg将Node.js项目打包为可执行文件的方法,希望对大家有所帮助!

一文解析package.json和package-lock.json一文解析package.json和package-lock.jsonSep 01, 2022 pm 08:02 PM

本篇文章带大家详解package.json和package-lock.json文件,希望对大家有所帮助!

分享一个Nodejs web框架:Fastify分享一个Nodejs web框架:FastifyAug 04, 2022 pm 09:23 PM

本篇文章给大家分享一个Nodejs web框架:Fastify,简单介绍一下Fastify支持的特性、Fastify支持的插件以及Fastify的使用方法,希望对大家有所帮助!

node爬取数据实例:聊聊怎么抓取小说章节node爬取数据实例:聊聊怎么抓取小说章节May 02, 2022 am 10:00 AM

node怎么爬取数据?下面本篇文章给大家分享一个node爬虫实例,聊聊利用node抓取小说章节的方法,希望对大家有所帮助!

手把手带你使用Node.js和adb开发一个手机备份小工具手把手带你使用Node.js和adb开发一个手机备份小工具Apr 14, 2022 pm 09:06 PM

本篇文章给大家分享一个Node实战,介绍一下使用Node.js和adb怎么开发一个手机备份小工具,希望对大家有所帮助!

图文详解node.js如何构建web服务器图文详解node.js如何构建web服务器Aug 08, 2022 am 10:27 AM

先介绍node.js的安装,再介绍使用node.js构建一个简单的web服务器,最后通过一个简单的示例,演示网页与服务器之间的数据交互的实现。

See all articles

熱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.能量晶體解釋及其做什麼(黃色晶體)
2 週前By尊渡假赌尊渡假赌尊渡假赌
倉庫:如何復興隊友
4 週前By尊渡假赌尊渡假赌尊渡假赌
Hello Kitty Island冒險:如何獲得巨型種子
4 週前By尊渡假赌尊渡假赌尊渡假赌

熱工具

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SecLists

SecLists

SecLists是最終安全測試人員的伙伴。它是一個包含各種類型清單的集合,這些清單在安全評估過程中經常使用,而且都在一個地方。 SecLists透過方便地提供安全測試人員可能需要的所有列表,幫助提高安全測試的效率和生產力。清單類型包括使用者名稱、密碼、URL、模糊測試有效載荷、敏感資料模式、Web shell等等。測試人員只需將此儲存庫拉到新的測試機上,他就可以存取所需的每種類型的清單。

Safe Exam Browser

Safe Exam Browser

Safe Exam Browser是一個安全的瀏覽器環境,安全地進行線上考試。該軟體將任何電腦變成一個安全的工作站。它控制對任何實用工具的訪問,並防止學生使用未經授權的資源。

EditPlus 中文破解版

EditPlus 中文破解版

體積小,語法高亮,不支援程式碼提示功能

mPDF

mPDF

mPDF是一個PHP庫,可以從UTF-8編碼的HTML產生PDF檔案。原作者Ian Back編寫mPDF以從他的網站上「即時」輸出PDF文件,並處理不同的語言。與原始腳本如HTML2FPDF相比,它的速度較慢,並且在使用Unicode字體時產生的檔案較大,但支援CSS樣式等,並進行了大量增強。支援幾乎所有語言,包括RTL(阿拉伯語和希伯來語)和CJK(中日韓)。支援嵌套的區塊級元素(如P、DIV),