這篇文章主要講解了“Node中的事件循環(huán)、process.nextTick()實例分析”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“Node中的事件循環(huán)、process.nextTick()實例分析”吧!
創(chuàng)新互聯(lián)是一家集網(wǎng)站建設,阿瓦提企業(yè)網(wǎng)站建設,阿瓦提品牌網(wǎng)站建設,網(wǎng)站定制,阿瓦提網(wǎng)站建設報價,網(wǎng)絡營銷,網(wǎng)絡優(yōu)化,阿瓦提網(wǎng)站推廣為一體的創(chuàng)新建站企業(yè),幫助傳統(tǒng)企業(yè)提升企業(yè)形象加強企業(yè)競爭力。可充分滿足這一群體相比中小企業(yè)更為豐富、高端、多元的互聯(lián)網(wǎng)需求。同時我們時刻保持專業(yè)、時尚、前沿,時刻以成就客戶成長自我,堅持不斷學習、思考、沉淀、凈化自己,讓我們?yōu)楦嗟钠髽I(yè)打造出實用型網(wǎng)站。

事件循環(huán)是 Node.js 處理非阻塞 I/O 操作的機制——盡管 JavaScript 是單線程處理的——當有可能的時候,它們會把操作轉移到系統(tǒng)內核中去。
既然目前大多數(shù)內核都是多線程的,它們可在后臺處理多種操作。當其中的一個操作完成的時候,內核通知 Node.js 將適合的回調函數(shù)添加到輪詢隊列中等待時機執(zhí)行。我們在本文后面會進行詳細介紹。
當 Node.js 啟動后,它會初始化事件循環(huán),處理已提供的輸入腳本(或丟入 REPL,本文不涉及到),它可能會調用一些異步的 API、調度定時器,或者調用 process.nextTick(),然后開始處理事件循環(huán)。
下面的圖表展示了事件循環(huán)操作順序的簡化概覽。
┌───────────────────────────┐ ┌─>│ timers │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ pending callbacks │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ idle, prepare │ │ └─────────────┬─────────────┘ ┌───────────────┐ │ ┌─────────────┴─────────────┐ │ incoming: │ │ │ poll │<─────┤ connections, │ │ └─────────────┬─────────────┘ │ data, etc. │ │ ┌─────────────┴─────────────┐ └───────────────┘ │ │ check │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ └──┤ close callbacks │ └───────────────────────────┘
注意:每個框被稱為事件循環(huán)機制的一個階段。
每個階段都有一個 FIFO 隊列來執(zhí)行回調。雖然每個階段都是特殊的,但通常情況下,當事件循環(huán)進入給定的階段時,它將執(zhí)行特定于該階段的任何操作,然后執(zhí)行該階段隊列中的回調,直到隊列用盡或最大回調數(shù)已執(zhí)行。當該隊列已用盡或達到回調限制,事件循環(huán)將移動到下一階段,等等。
由于這些操作中的任何一個都可能調度_更多的_操作和由內核排列在輪詢階段被處理的新事件, 且在處理輪詢中的事件時,輪詢事件可以排隊。因此,長時間運行的回調可以允許輪詢階段運行長于計時器的閾值時間。有關詳細信息,請參閱 計時器和 輪詢部分。
注意:在 Windows 和 Unix/Linux 實現(xiàn)之間存在細微的差異,但這對演示來說并不重要。最重要的部分在這里。實際上有七或八個步驟,但我們關心的是 Node.js 實際上使用以上的某些步驟。
定時器:本階段執(zhí)行已經(jīng)被 setTimeout() 和 setInterval() 的調度回調函數(shù)。
待定回調:執(zhí)行延遲到下一個循環(huán)迭代的 I/O 回調。
idle, prepare:僅系統(tǒng)內部使用。
輪詢:檢索新的 I/O 事件;執(zhí)行與 I/O 相關的回調(幾乎所有情況下,除了關閉的回調函數(shù),那些由計時器和 setImmediate() 調度的之外),其余情況 node 將在適當?shù)臅r候在此阻塞。
檢測:setImmediate() 回調函數(shù)在這里執(zhí)行。
關閉的回調函數(shù):一些關閉的回調函數(shù),如:socket.on('close', ...)。
在每次運行的事件循環(huán)之間,Node.js 檢查它是否在等待任何異步 I/O 或計時器,如果沒有的話,則完全關閉。
計時器指定可以執(zhí)行所提供回調的 閾值,而不是用戶希望其執(zhí)行的確切時間。在指定的一段時間間隔后, 計時器回調將被盡可能早地運行。但是,操作系統(tǒng)調度或其它正在運行的回調可能會延遲它們。
注意:輪詢階段 控制何時定時器執(zhí)行。
例如,假設您調度了一個在 100 毫秒后超時的定時器,然后您的腳本開始異步讀取會耗費 95 毫秒的文件:
const fs = require('fs');
function someAsyncOperation(callback) {
// Assume this takes 95ms to complete
fs.readFile('/path/to/file', callback);
}
const timeoutScheduled = Date.now();
setTimeout(() => {
const delay = Date.now() - timeoutScheduled;
console.log(`${delay}ms have passed since I was scheduled`);
}, 100);
// do someAsyncOperation which takes 95 ms to complete
someAsyncOperation(() => {
const startCallback = Date.now();
// do something that will take 10ms...
while (Date.now() - startCallback < 10) {
// do nothing
}
});當事件循環(huán)進入 輪詢階段時,它有一個空隊列(此時 fs.readFile() 尚未完成),因此它將等待剩下的毫秒數(shù),直到達到最快的一個計時器閾值為止。當它等待 95 毫秒過后時,fs.readFile() 完成讀取文件,它的那個需要 10 毫秒才能完成的回調,將被添加到 輪詢隊列中并執(zhí)行。當回調完成時,隊列中不再有回調,因此事件循環(huán)機制將查看最快到達閾值的計時器,然后將回到 計時器階段,以執(zhí)行定時器的回調。在本示例中,您將看到調度計時器到它的回調被執(zhí)行之間的總延遲將為 105 毫秒。
注意:為了防止 輪詢階段餓死事件循環(huán),libuv(實現(xiàn) Node.js 事件循環(huán)和平臺的所有異步行為的 C 函數(shù)庫),在停止輪詢以獲得更多事件之前,還有一個硬性最大值(依賴于系統(tǒng))。
此階段對某些系統(tǒng)操作(如 TCP 錯誤類型)執(zhí)行回調。例如,如果 TCP 套接字在嘗試連接時接收到 ECONNREFUSED,則某些 *nix 的系統(tǒng)希望等待報告錯誤。這將被排隊以在 掛起的回調階段執(zhí)行。
輪詢階段有兩個重要的功能:
計算應該阻塞和輪詢 I/O 的時間。
然后,處理 輪詢隊列里的事件。
當事件循環(huán)進入 輪詢階段且_沒有被調度的計時器時_,將發(fā)生以下兩種情況之一:
如果 輪詢隊列 不是空的
,事件循環(huán)將循環(huán)訪問回調隊列并同步執(zhí)行它們,直到隊列已用盡,或者達到了與系統(tǒng)相關的硬性限制。
如果 輪詢隊列 是空的,還有兩件事發(fā)生:
如果腳本被 setImmediate() 調度,則事件循環(huán)將結束 輪詢階段,并繼續(xù) 檢查階段以執(zhí)行那些被調度的腳本。
如果腳本 未被setImmediate()調度,則事件循環(huán)將等待回調被添加到隊列中,然后立即執(zhí)行。
一旦 輪詢隊列為空,事件循環(huán)將檢查 _已達到時間閾值的計時器_。如果一個或多個計時器已準備就緒,則事件循環(huán)將繞回計時器階段以執(zhí)行這些計時器的回調。
此階段允許人員在輪詢階段完成后立即執(zhí)行回調。如果輪詢階段變?yōu)榭臻e狀態(tài),并且腳本使用 setImmediate() 后被排列在隊列中,則事件循環(huán)可能繼續(xù)到 檢查階段而不是等待。
setImmediate() 實際上是一個在事件循環(huán)的單獨階段運行的特殊計時器。它使用一個 libuv API 來安排回調在 輪詢階段完成后執(zhí)行。
通常,在執(zhí)行代碼時,事件循環(huán)最終會命中輪詢階段,在那等待傳入連接、請求等。但是,如果回調已使用 setImmediate()調度過,并且輪詢階段變?yōu)榭臻e狀態(tài),則它將結束此階段,并繼續(xù)到檢查階段而不是繼續(xù)等待輪詢事件。
如果套接字或處理函數(shù)突然關閉(例如 socket.destroy()),則'close' 事件將在這個階段發(fā)出。否則它將通過 process.nextTick() 發(fā)出。
setImmediate() 和 setTimeout() 很類似,但是基于被調用的時機,他們也有不同表現(xiàn)。
setImmediate() 設計為一旦在當前 輪詢階段完成, 就執(zhí)行腳本。
setTimeout() 在最小閾值(ms 單位)過后運行腳本。
執(zhí)行計時器的順序將根據(jù)調用它們的上下文而異。如果二者都從主模塊內調用,則計時器將受進程性能的約束(這可能會受到計算機上其他正在運行應用程序的影響)。
例如,如果運行以下不在 I/O 周期(即主模塊)內的腳本,則執(zhí)行兩個計時器的順序是非確定性的,因為它受進程性能的約束:
// timeout_vs_immediate.js
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
$ node timeout_vs_immediate.js
timeout
immediate
$ node timeout_vs_immediate.js
immediate
timeout但是,如果你把這兩個函數(shù)放入一個 I/O 循環(huán)內調用,setImmediate 總是被優(yōu)先調用:
// timeout_vs_immediate.js
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
$ node timeout_vs_immediate.js
immediate
timeout
$ node timeout_vs_immediate.js
immediate
timeout使用 setImmediate() 相對于setTimeout() 的主要優(yōu)勢是,如果setImmediate()是在 I/O 周期內被調度的,那它將會在其中任何的定時器之前執(zhí)行,跟這里存在多少個定時器無關
您可能已經(jīng)注意到 process.nextTick() 在圖示中沒有顯示,即使它是異步 API 的一部分。這是因為 process.nextTick() 從技術上講不是事件循環(huán)的一部分。相反,它都將在當前操作完成后處理 nextTickQueue, 而不管事件循環(huán)的當前階段如何。這里的一個_操作_被視作為一個從底層 C/C++ 處理器開始過渡,并且處理需要執(zhí)行的 JavaScript 代碼。
回顧我們的圖示,任何時候在給定的階段中調用 process.nextTick(),所有傳遞到 process.nextTick() 的回調將在事件循環(huán)繼續(xù)之前解析。這可能會造成一些糟糕的情況,因為它允許您通過遞歸 process.nextTick()調用來“餓死”您的 I/O,阻止事件循環(huán)到達 輪詢階段。
為什么這樣的事情會包含在 Node.js 中?它的一部分是一個設計理念,其中 API 應該始終是異步的,即使它不必是。以此代碼段為例:
function apiCall(arg, callback) {
if (typeof arg !== 'string')
return process.nextTick(
callback,
new TypeError('argument should be string')
);
}代碼段進行參數(shù)檢查。如果不正確,則會將錯誤傳遞給回調函數(shù)。最近對 API 進行了更新,允許傳遞參數(shù)給 process.nextTick(),這將允許它接受任何在回調函數(shù)位置之后的參數(shù),并將參數(shù)傳遞給回調函數(shù)作為回調函數(shù)的參數(shù),這樣您就不必嵌套函數(shù)了。
我們正在做的是將錯誤傳回給用戶,但僅在執(zhí)行用戶的其余代碼之后。通過使用process.nextTick(),我們保證 apiCall() 始終在用戶代碼的其余部分_之后_和在讓事件循環(huán)繼續(xù)進行_之前_,執(zhí)行其回調函數(shù)。為了實現(xiàn)這一點,JS 調用棧被允許展開,然后立即執(zhí)行提供的回調,允許進行遞歸調用 process.nextTick(),而不觸碰 RangeError: 超過 V8 的最大調用堆棧大小 限制。
這種設計原理可能會導致一些潛在的問題。 以此代碼段為例:
let bar;
// this has an asynchronous signature, but calls callback synchronously
function someAsyncApiCall(callback) {
callback();
}
// the callback is called before `someAsyncApiCall` completes.
someAsyncApiCall(() => {
// since someAsyncApiCall has completed, bar hasn't been assigned any value
console.log('bar', bar); // undefined
});
bar = 1;用戶將 someAsyncApiCall() 定義為具有異步簽名,但實際上它是同步運行的。當調用它時,提供給 someAsyncApiCall() 的回調是在事件循環(huán)的同一階段內被調用,因為 someAsyncApiCall() 實際上并沒有異步執(zhí)行任何事情。結果,回調函數(shù)在嘗試引用 bar,但作用域中可能還沒有該變量,因為腳本尚未運行完成。
通過將回調置于 process.nextTick() 中,腳本仍具有運行完成的能力,允許在調用回調之前初始化所有的變量、函數(shù)等。它還具有不讓事件循環(huán)繼續(xù)的優(yōu)點,適用于讓事件循環(huán)繼續(xù)之前,警告用戶發(fā)生錯誤的情況。下面是上一個使用 process.nextTick() 的示例:
let bar;
function someAsyncApiCall(callback) {
process.nextTick(callback);
}
someAsyncApiCall(() => {
console.log('bar', bar); // 1
});
bar = 1;這又是另外一個真實的例子:
const server = net.createServer(() => {}).listen(8080);
server.on('listening', () => {});只有傳遞端口時,端口才會立即被綁定。因此,可以立即調用 'listening' 回調。問題是 .on('listening') 的回調在那個時間點尚未被設置。
為了繞過這個問題,'listening' 事件被排在 nextTick() 中,以允許腳本運行完成。這讓用戶設置所想設置的任何事件處理器。
就用戶而言,我們有兩個類似的調用,但它們的名稱令人費解。
process.nextTick() 在同一個階段立即執(zhí)行。
setImmediate() 在事件循環(huán)的接下來的迭代或 'tick' 上觸發(fā)。
實質上,這兩個名稱應該交換,因為 process.nextTick() 比 setImmediate() 觸發(fā)得更快,但這是過去遺留問題,因此不太可能改變。如果貿然進行名稱交換,將破壞 npm 上的大部分軟件包。每天都有更多新的模塊在增加,這意味著我們要多等待每一天,則更多潛在破壞會發(fā)生。盡管這些名稱使人感到困惑,但它們本身名字不會改變。
我們建議開發(fā)人員在所有情況下都使用 setImmediate(),因為它更容易理解。
有兩個主要原因:
允許用戶處理錯誤,清理任何不需要的資源,或者在事件循環(huán)繼續(xù)之前重試請求。
有時有讓回調在棧展開后,但在事件循環(huán)繼續(xù)之前運行的必要。
以下是一個符合用戶預期的簡單示例:
const server = net.createServer();
server.on('connection', (conn) => {});
server.listen(8080);
server.on('listening', () => {});假設 listen() 在事件循環(huán)開始時運行,但 listening 的回調被放置在 setImmediate() 中。除非傳遞過主機名,才會立即綁定到端口。為使事件循環(huán)繼續(xù)進行,它必須命中 輪詢階段,這意味著有可能已經(jīng)接收了一個連接,并在偵聽事件之前觸發(fā)了連接事件。
另一個示例運行的函數(shù)構造函數(shù)是從 EventEmitter 繼承的,它想調用構造函數(shù):
const EventEmitter = require('events');
const util = require('util');
function MyEmitter() {
EventEmitter.call(this);
this.emit('event');
}
util.inherits(MyEmitter, EventEmitter);
const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
console.log('an event occurred!');
});你不能立即從構造函數(shù)中觸發(fā)事件,因為腳本尚未處理到用戶為該事件分配回調函數(shù)的地方。因此,在構造函數(shù)本身中可以使用 process.nextTick() 來設置回調,以便在構造函數(shù)完成后發(fā)出該事件,這是預期的結果:
const EventEmitter = require('events');
const util = require('util');
function MyEmitter() {
EventEmitter.call(this);
// use nextTick to emit the event once a handler is assigned
process.nextTick(() => {
this.emit('event');
});
}
util.inherits(MyEmitter, EventEmitter);
const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
console.log('an event occurred!');
});感謝各位的閱讀,以上就是“Node中的事件循環(huán)、process.nextTick()實例分析”的內容了,經(jīng)過本文的學習后,相信大家對Node中的事件循環(huán)、process.nextTick()實例分析這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是創(chuàng)新互聯(lián),小編將為大家推送更多相關知識點的文章,歡迎關注!
網(wǎng)頁標題:Node中的事件循環(huán)、process.nextTick()實例分析
瀏覽地址:http://chinadenli.net/article2/jiieoc.html
成都網(wǎng)站建設公司_創(chuàng)新互聯(lián),為您提供自適應網(wǎng)站、網(wǎng)站內鏈、App設計、微信小程序、全網(wǎng)營銷推廣、網(wǎng)站改版
聲明:本網(wǎng)站發(fā)布的內容(圖片、視頻和文字)以用戶投稿、用戶轉載內容為主,如果涉及侵權請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內容未經(jīng)允許不得轉載,或轉載時需注明來源: 創(chuàng)新互聯(lián)