摘要:如果一個即時定時器是被一個正在執行的回調排入隊列的,則該定時器直到下一次事件循環迭代才會被觸發。參數描述在事件循環的當前回合結束時要調用的函數。事件輪詢隨后的調用,會在任何事件包括定時器之前運行。
系列文章
Nodejs高性能原理(上) --- 異步非阻塞事件驅動模型
Nodejs高性能原理(下) --- 事件循環詳解
終于開始我nodejs的博客生涯了,先從基本的原理講起.以前寫過一篇瀏覽器執行機制的文章,和nodejs的相似之處還是挺多的,不熟悉可以去看看先.
Javascript執行機制--單線程,同異步任務,事件循環
寫下來之后可能還是有點懞,以后慢慢補充,也歡迎指正,特別是那篇翻譯文章后面已經看不懂了.有人出手科普一下就好了.因為懶得動手做,整篇文章的圖片要么來源官網,要么來源百度圖片.
補充: 當前Nodejs版本10.3.0
2019/8/13 修改部分描述內容
基本來自The Node.js Event Loop, Timers, and process.nextTick(),可以說這部分我就是翻譯功能,部分翻譯太繞口會和諧一下,基本忠于原文.
當nodejs開始運行的時候會初始化事件循環,處理所提供的輸入腳本或者放置進REPL(Read Eval Print Loop:交互式解釋器類似 Window 系統的終端或 Unix/Linux shell),可能會進行異步API調用.定時器調度,或者process.nextTick(),然后開始處理事件循環的流程.
下面來自官網的炫酷流程代碼示意圖(官網直接用符號拼湊出來,這里因為編輯器問題衹能截圖)
注意: 每個框都被稱為事件循環的一個流程階段.
每個階段都有一個FIFO(先進先出)執行回調函數的隊列,然而每個階段都有其獨特之處.通常當事件循環進入到給定階段會執行特定于該階段的所有操作.然后執行該階段隊列的回調事件直到隊列耗盡或者超過最大執行限度為止,然后事件循環就會走向下一階段,以此類推.
因為這些操作可能會調度更多的操作并且在poll階段中新的處理事件會加入到內核的隊列,即處理輪詢事件時候又加入新的輪詢事件,因此,長時間運行回調事件會讓poll階段運行時間超過定時器的閾值.
階段綜述:timers(定時器): 這階段執行setTimeout和setInterval調度的回調;
pending callbacks(等待回調): 推遲到下一次循環迭代執行I/O回調;
idle,prepare(閑置,準備): 只能內部使用;
poll(輪詢): 檢索新的I/O事件;執行I/O相關回調(除了close callbacks以外,大多數是定時器調度,和setImmediate()),當運行時候適當條件下nodejs會占用阻塞;
check(檢測): setImmediate()回調就在這執行;
close callbacks(關閉回調): 一些關閉回調,例如socket.on("close", ...),
在事件循環的每次運行過程中,nodejs會檢測是否有任何待處理的異步I/O或者定時器,沒有的話就徹底清除關閉.
Timers(定時器)在定時器設定了一個閾值之后,被提供的回調函數實際執行時間可能不是開發者想要它被執行的時間,定時器回調會在指定閾值過去后盡可能早的運行,然而操作系統調度或者其他回調運行都可能會導致延遲.
注意: 為了防止輪詢階段持續時間太長,libuv 會根據操作系統的不同設置一個輪詢的上限。(這就是為什么上面會說執行該階段隊列的回調事件直到隊列耗盡或者超過最大執行限度為止)
(下面會多帶帶詳細講解定時器的東西)
這階段會執行一些系統操作回調像TCP錯誤類型,例如當一個TCP socket想要連接的時候接收到ECONNREFUSED,一些*nix系統會等待錯誤報文,這會被排在pending callbacks 階段執行.
poll(輪詢)這階段有兩個主要功能:
計算它應該阻塞多長的時間和進行輪詢I/O操作;
(原文: Calculating how long it should block and poll for I/O, then,我看到有些人會翻譯成當 timers 的定時器到期后,執行定時器(setTimeout 和 setInterval)的 callback。不知道版本不對還是我翻譯不對味)
處理poll隊列事件;
當事件循環進入poll階段,并且沒有timers調度,會發生其中一種情況:
如果poll隊列不為空,事件循環會迭代回調隊列同步執行它們直到隊列耗盡或者到達系統限制;
如果poll隊列為空,一件或者多件情況會發生:
如果setImmediate()腳本已經被調度,事件循環的poll階段完成然后繼續到check階段去執行那里的調度腳本;
如果setImmediate()腳本還沒被調度,事件循環會等待回調被添加到隊列,然后立即執行.
一旦poll隊列清空了事件循環會檢測有沒有定時器閾值是否到達,如果一個或多個定時器已經準備好,事件循環會繞回到timers階段去執行它們的定時器回調函數.
check(檢測)這階段允許開發者在poll階段完成之后立即執行回調函數,如果poll階段在閑置中并且腳本已經被setImmediate()加入隊列,事件循環會跳到check階段而不是等待.
setImmediate()實際上是一個特殊的定時器,它會在事件循環的多帶帶階段運行.通過libuv API在poll階段完成之后調度回調去執行.
一般來說,當代碼執行完,事件循環最終會到達poll階段去等待即將到來的連接,請求等等.然而,如果一個回調函數被setImmediate()調度并且poll階段是閑置狀態,它會結束并且跳到check階段而不是在等待輪詢事件.
close callbacks(關閉回調)如果一個socket或handle突然被關閉(例如socket.destroy()),"close"事件會在這階段被觸發,否則會通過process.nextTick()被觸發.
非異步API(強勢插樓)事件循環階段部分已經講完了,剩下的是定時器之間區別部分,在那之前我想在這里補充一下定時器知識!
Node.js 中的計時器函數實現使用了一個與瀏覽器類似但不同的內部實現,它是基于 Node.js 事件循環構建的。
瀏覽器定時器 setTimeout(callback,delay,lang) :在指定的毫秒數后調用函數或計算表達式,返回一個用于 clearTimeout() 的Timeout或窗口被關閉。
參數 | 描述 | ||
---|---|---|---|
callback | 必需。要調用的函數后要執行的 JavaScript 代碼串。 | ||
delay | 必需。在執行代碼前需等待的毫秒數, W3C標準規定時間間隔低于4ms被算為4ms,具體看瀏覽器 | ||
lang | 可選。腳本語言可以是:JScript | VBScript | JavaScript |
按照指定的周期(以毫秒計)來調用函數或計算表達式。方法會不停地調用函數,返回一個用于 clearInterval() 的Timeout或窗口被關閉。
參數請看上面setTimeout.
在指定的毫秒數后調用函數或計算表達式,返回一個用于 clearTimeout() 的Timeout或窗口被關閉。
參數 | 描述 |
---|---|
callback | 必需。要調用的函數后要執行的 JavaScript 代碼串。 |
delay | 必需。在執行代碼前需等待的毫秒數。當 delay 大于 2147483647 或小于 1 時,delay 會被設為 1。 |
...args | 可選, 當調用 callback 時要傳入的可選參數。 |
此外還增加一些方法timeout.ref(),timeout.unref()等,請自行查看.Timeout 類
setInterval(callback, delay[, ...args])按照指定的周期(以毫秒計)來調用函數或計算表達式。方法會不停地調用函數,返回一個用于 clearInterval() 的Timeout或窗口被關閉。
參數請看上面setTimeout.
預定立即執行的 callback,它是在 I/O 事件的回調之后被觸發。 返回一個用于 clearImmediate() 的 Immediate。
當多次調用 setImmediate() 時,callback 函數會按照它們被創建的順序依次執行。 每次事件循環迭代都會處理整個回調隊列。 如果一個即時定時器是被一個正在執行的回調排入隊列的,則該定時器直到下一次事件循環迭代才會被觸發。
參數 | 描述 |
---|---|
callback | 在 Node.js 事件循環的當前回合結束時要調用的函數。 |
...args | 可選, 當調用 callback 時要傳入的可選參數。 |
對應的清除方法clearImmediate(),此外還增加一些方法setImmediate.ref(),setImmediate.unref()等,請自行查看.Immediate 類
promise寫法(題外話)可用util.promisify()提供的promises常用變體
const util = require("util"); const setTimeoutPromise = util.promisify(setTimeout), setImmediatePromise = util.promisify(setImmediate); setTimeoutPromise(40, "foobar").then(value => { // value === "foobar" (passing values is optional) // This is executed after about 40 milliseconds. }); setImmediatePromise("foobar").then(value => { // value === "foobar" (passing values is optional) // This is executed after all I/O callbacks. }); // or with async function async function timerExample() { console.log("Before I/O callbacks"); await setImmediatePromise(); console.log("After I/O callbacks"); } timerExample();process.nextTick(callback[, ...args])
將 callback 添加到next tick 隊列。 一旦當前事件輪詢隊列的任務全部完成,在next tick隊列中的所有callbacks會被依次調用。但是不同于上面的定時器.在內部的處理機制不同,nextTick擁有比延時更多的特性.
注意:這不是定時器,而且遞歸調用nextTick callbacks 會阻塞任何I/O操作,就像一個while(true)循環一樣
參數 | 描述 |
---|---|
callback | 一旦當前事件輪詢隊列的任務全部完成,在next tick隊列中要調用的函數 |
...args | 可選, 當調用 callback 時要傳入的可選參數。 |
console.log("start"); process.nextTick(() => { console.log("nextTick callback"); }); console.log("scheduled"); // Output: // start // scheduled // nextTick callback在對象構造好但還沒有任何I/O發生之前,想給用戶機會來指定某些事件處理器。
function MyThing(options) { this.setupOptions(options); process.nextTick(() => { this.startDoingStuff(); }); } const thing = new MyThing(); thing.getReadyForStuff(); // thing.startDoingStuff() gets called now, not before.每次事件輪詢后,在額外的I/O執行前,next tick隊列都會優先執行。
//使用場景 const maybeTrue = Math.random() > 0.5; maybeSync(maybeTrue, () => { foo(); }); bar(); //危險寫法,因為不清楚foo() 或 bar() 哪個會被先調用 function maybeSync(arg, cb) { if (arg) { cb(); return; } fs.stat("file", cb); } //優化寫法,每次事件輪詢后,在額外的I/O執行前,next tick隊列都會優先執行 function definitelyAsync(arg, cb) { if (arg) { process.nextTick(cb); return; } fs.stat("file", cb); }繼續回到文章 setImmediate() vs setTimeout()
setImmediate() vs setTimeout()很相似,但是行為方式的不同取決于他們調用時機.
setImmediate()被設計為在當前poll階段完成之后執行腳本.
setTimeout()會在消耗一段時間閾值之后調度一段腳本去運行.
定時器被執行時候的順序變化取決于它們被調用時候的上下文,如果都是在主模塊內部被調用會受到進程性能的約束(可能被本機其他應用運行影響);
例如,如果我們不在I/O循環運行下面的腳本(也就是在主模塊中),兩個定時器的執行順序是不確定的,因為它們受到進程性能的約束.
// timeout_vs_immediate.js setTimeout(function timeout () { console.log("timeout"); },0); setImmediate(function immediate () { console.log("immediate"); });
$ node timeout_vs_immediate.js
timeout
immediate$ node timeout_vs_immediate.js
immediate
timeout
可是如果你把兩個代碼放進I/O循環內部,immediate()回調函數總是先執行;
// 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()的主要優勢是如果在I/O循環內部調用,setImmediate()總會在所有定時器之前執行,與你定義多少個定時器無關.
理解process.nextTick()你可能已經注意到process.nextTick()并沒有出現在圖表,雖然它是異步API的一部分,那是因為process.nextTick()技術上不是事件循環部分.相反,process.nextTick()會在當前操作完成之后被處理,不管事件循環的當前階段如何.
回顧我們的圖表,在給定階段的任何時候你調用process.nextTick(),傳遞給process.nextTick()的回調函數都會在事件循環繼續之前被解決,這會造成一些糟糕情況因為它允許你通過執行遞歸process.nextTick()調用去"餓死"(starve)你的I/O,從而阻止事件循環到達poll階段.
為什么會被允許?為什么一些像這樣的內容會被包含在Nodejs?這部分是因為它是一種設計哲學,API應該總是異步即使它并不需要,看這段代碼片段例子
function apiCall(arg, callback) { if (typeof arg !== "string") return process.nextTick(callback, new TypeError("argument should be string")); }
這片段會檢查入參,如果不正確會傳遞錯誤到回調函數,最近更新的API允許傳遞入參到process.nextTick(),允許他在回調后取傳遞的任何參數作為回調的入參,這樣就不必嵌套函數了.
這句又長又繞口,附上部分原文:
The API updated fairly recently to allow passing arguments to process.nextTick() allowing it to take any arguments passed after the callback to be propagated as the arguments to the callback so you don"t have to nest functions.
我們要做的是傳遞一個錯誤給開發者但僅僅是我們已經允許開發者其余的代碼執行之后.通過使用process.nextTick()我們保證apiCall()總會在開發者其余代碼執行之后事件循環允許執行之前運行它的回調函數,為了實現這一步,JS調用堆棧允許展開立即執行所提供的回調函數,允許開發者執行遞歸調用process.nextTick()而不會達到引用錯誤: Maximum call stack size exceeded from v8.
這句又長又繞口,附上原文:
What we"re doing is passing an error back to the user but only after we have allowed the rest of the user"s code to execute. By using process.nextTick() we guarantee that apiCall() always runs its callback after the rest of the user"s code and before the event loop is allowed to proceed. To achieve this, the JS call stack is allowed to unwind then immediately execute the provided callback which allows a person to make recursive calls to process.nextTick() without reaching a RangeError: Maximum call stack size exceeded from 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()有一個異步簽名(signature??),實際上卻是同步操作,當它調用時候提供給someAsyncApiCall()的回調函數會在事件循環的相同階段被調用因為someAsyncApiCall()實際上并沒有做任何異步事情,結果回調函數試著去引用bar即使它可能還沒在作用域里,因為代碼不可能運行完成.
但是如果把它放進process.nextTick(),代碼依舊有能力跑完,允許所有變量,函數等等在回調函數被調用之前優先初始化完,它具有不讓事件循環繼續的優點,在允許事件循環繼續之前,提醒用戶注意錯誤可能是有用的。
這句又長又繞口,附上原文:
The user defines someAsyncApiCall() to have an asynchronous signature, but it actually operates synchronously. When it is called, the callback provided to someAsyncApiCall() is called in the same phase of the event loop because someAsyncApiCall() doesn"t actually do anything asynchronously. As a result, the callback tries to reference bar even though it may not have that variable in scope yet, because the script has not been able to run to completion.By placing the callback in a process.nextTick(), the script still has the ability to run to completion, allowing all the variables, functions, etc., to be initialized prior to the callback being called. It also has the advantage of not allowing the event loop to continue. It may be useful for the user to be alerted to an error before the event loop is allowed to continue. Here is the previous example using process.nextTick():
這是上面使用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", () => {});
(這句又長又繞口,不想翻了:)
When only a port is passed, the port is bound immediately. So, the "listening" callback could be called immediately. The problem is that the .on("listening") callback will not have been set by that time.
想要避開這問題,"listening"事件會加入nextTick()隊列以容許腳本運行完,這允許開發者設置任何他們想要的任何事件處理器.
process.nextTick() vs setImmediate()就用戶而言,我們有兩個類似的調用,不過他們的名字令人困惑.
process.nextTick() 在同一階段立刻觸發(原文fires: 點燃;解雇;開除;使發光;燒制;激動;放槍???)
setImmediate() 在事件循環的后續迭代或“tick”中觸發(原文fires)
本質上,名字應該調換,process.nextTick()比setImmediate()更加容易觸發,但這是一種不可變得的過去的產物,這種轉換會在npm中破壞大量的包,每天都有很多新包被添加,意味著我們每等待一天就有更多潛在的破壞發生,即使它們多困惑也不能更改它們的名字.
我們建議開發者們在任何情況使用setImmediate()因為它容易推出(reason about??)(它會讓代碼兼容更廣泛的環境變量,像browser JS)
為什么使用process.nextTick()?(翻譯文章最后內容)兩個原因:
1, 允許開發者們處理錯誤,清除任何不需要的資源,或者嘗試在事件循環繼續之前再次發起請求.
2, 在需要的時候允許調用棧釋放(unwound??)之后但事件循環繼續之前運行一個回調函數.
一個符合開發者們期望的簡單例子
const server = net.createServer(); server.on("connection", (conn) => { }); server.listen(8080); server.on("listening", () => { });
假設listen()在事件循環開始的時候運行,但是監聽回調被放置在setImmediate()。除非傳遞主機名立即綁定端口,想讓事件循環繼續進行必須進入poll階段,意味著有機會(a non-zero chance??)已經接收到一個連接,允許在監聽事件之前觸發連接事件。
(有段名詞不懂怎么翻譯:)
which means there is a non-zero chance that a connection could have been received allowing the connection event to be fired before the listening event
另一個例子是運行構造函數,從EventEmitter繼承并且想要在構造函數內部調用一個事件。
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!"); });
我們不能在構造函數立刻發出事件是因為腳本可能還沒處理到開發者設置觸發事件回調函數的位置,所以在構造函數內部本身你能使用process.nextTick()設置觸發事件回調函數以在構造函數已經完成之后提供期望結果。
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!"); });輸出例子
你們試試看這個輸出順序符不符合你們預期
const fs = require("fs"); console.log("start"); setTimeout(function timeout() { console.log("模塊外部timeout"); }, 1000); setImmediate(function immediate() { console.log("模塊外部immediate"); }); process.nextTick(() => { console.log("模塊外部nextTick callback"); }); fs.readFile(__filename, () => { setTimeout(function timeout() { console.log("I/O內部timeout"); }, 0); setImmediate(function immediate() { console.log("I/O內部immediate"); }); process.nextTick(() => { console.log("I/O內部nextTick callback"); }); }); console.log("end"); // start // end // 模塊外部nextTick callback // 模塊外部immediate // I/O內部nextTick callback // I/O內部immediate // I/O內部timeout // 模塊外部timeoutNodejs劣勢
總的來說單線程的鍋.
1, 異常拋出終止
我們都知道Javascript是一門單線程語言,在發生各種錯誤之后,JavaScript引擎通常會停止,并拋出一個錯誤.
Nodejs具體錯誤直接看Error (錯誤).
暫時還沒研究到,但是肯定可以通過一些方法解決的,后補.
2, 不適合CPU密集型
盡管我們上面已經提出了事件驅動異步IO非阻塞模型的各種優點,但是里面有個關鍵詞叫"I/O",如果是非I/O的處理例如CPU計算還是沒改進的,如果有長時間運行的計算,將會導致CPU時間片不能釋放,使得后續I/O無法發起.
可以通過把密集運算拆分成多個小任務,減輕CPU壓力.
3, 不能用到CPU的多核
現在的服務器操作系統基本都是支持多CPU/核了,單線程言語注定只能占用一個資源,不能充分利用.
解決單線程痛點方案
可以新開進程去玩,還沒研究到不說.
process - 進程
Node.js 中文網 API
The Node.js Event Loop, Timers, and process.nextTick()
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/106704.html
摘要:使用了一個事件驅動非阻塞式的模型,使其輕量又高效。的包管理器,是全球最大的開源庫生態系統。按照這個定義,之前所述的阻塞,非阻塞,多路復用信號驅動都屬于同步。 系列文章 Nodejs高性能原理(上) --- 異步非阻塞事件驅動模型Nodejs高性能原理(下) --- 事件循環詳解 前言 終于開始我nodejs的博客生涯了,先從基本的原理講起.以前寫過一篇瀏覽器執行機制的文章,和nodej...
摘要:整理收藏一些優秀的文章及大佬博客留著慢慢學習原文協作規范中文技術文檔協作規范阮一峰編程風格凹凸實驗室前端代碼規范風格指南這一次,徹底弄懂執行機制一次弄懂徹底解決此類面試問題瀏覽器與的事件循環有何區別筆試題事件循環機制異步編程理解的異步 better-learning 整理收藏一些優秀的文章及大佬博客留著慢慢學習 原文:https://www.ahwgs.cn/youxiuwenzhan...
摘要:開始執行文件,同步代碼執行完畢后,進入事件循環。時間未到的時候,如果有事件返回,就執行該事件注冊的回調函數。對于多次執行輸出結果不同,需要了解事件循環的基礎問題。 1. 說明 nodejs是單線程執行的,同時它又是基于事件驅動的非阻塞IO編程模型。這就使得我們不用等待異步操作結果返回,就可以繼續往下執行代碼。當異步事件觸發之后,就會通知主線程,主線程執行相應事件的回調。 本篇文章講解n...
摘要:概述本篇主要介紹的運行機制單線程事件循環結論先在中利用運行至完成和非阻塞完成單線程下異步任務的處理就是先處理主模塊主線程上的同步任務再處理異步任務異步任務使用事件循環機制完成調度涉及的內容有單線程事件循環同步執行異步執行定時器的事件循環開始 1.概述 本篇主要介紹JavaScript的運行機制:單線程事件循環(Event Loop). 結論先: 在JavaScript中, 利用運行至...
摘要:的單線程,與它的用途有關。特點的顯著特點異步機制事件驅動。隊列的讀取輪詢線程,事件的消費者,的主角。它將不同的任務分配給不同的線程,形成一個事件循環,以異步的方式將任務的執行結果返回給引擎。 這兩天跟同事同事討論遇到的一個問題,js中的event loop,引出了chrome與node中運行具有setTimeout和Promise的程序時候執行結果不一樣的問題,從而引出了Nodejs的...
閱讀 1649·2021-11-16 11:44
閱讀 2393·2021-10-11 11:07
閱讀 4036·2021-10-09 09:41
閱讀 663·2021-09-22 15:52
閱讀 3187·2021-09-09 09:33
閱讀 2701·2019-08-30 15:55
閱讀 2284·2019-08-30 15:55
閱讀 837·2019-08-30 15:55