實(shí)例分析JS與Node.js中的事件循環(huán)
這兩天跟同事同事討論遇到的一個(gè)問(wèn)題,js中的event loop,引出了chrome與node中運(yùn)行具有setTimeout和Promise的程序時(shí)候執(zhí)行結(jié)果不一樣的問(wèn)題,從而引出了Nodejs的event loop機(jī)制,記錄一下,感覺(jué)還是蠻有收獲的
console.log(1)
setTimeout(function() {
new Promise(function(resolve, reject) {
console.log(2)
resolve()
})
.then(() => {
console.log(3)
})
}, 0)
setTimeout(function() {
console.log(4)
}, 0)
// chrome中運(yùn)行:1 2 3 4
// Node中運(yùn)行: 1 2 4 3
chrome和Node執(zhí)行的結(jié)果不一樣,這就很有意思了。
1. JS 中的任務(wù)隊(duì)列
JavaScript語(yǔ)言的一大特點(diǎn)就是單線程,也就是說(shuō),同一個(gè)時(shí)間只能做一件事。那么,為什么JavaScript不能有多個(gè)線程呢?這樣能提高效率啊。
JavaScript的單線程,與它的用途有關(guān)。作為瀏覽器腳本語(yǔ)言,JavaScript的主要用途是與用戶互動(dòng),以及操作DOM。這決定了它只能是單線程,否則會(huì)帶來(lái)很復(fù)雜的同步問(wèn)題。比如,假定JavaScript同時(shí)有兩個(gè)線程,一個(gè)線程在某個(gè)DOM節(jié)點(diǎn)上添加內(nèi)容,另一個(gè)線程刪除了這個(gè)節(jié)點(diǎn),這時(shí)瀏覽器應(yīng)該以哪個(gè)線程為準(zhǔn)?
所以,為了避免復(fù)雜性,從一誕生,JavaScript就是單線程,這已經(jīng)成了這門語(yǔ)言的核心特征,將來(lái)也不會(huì)改變。
為了利用多核CPU的計(jì)算能力,HTML5提出Web Worker標(biāo)準(zhǔn),允許JavaScript腳本創(chuàng)建多個(gè)線程,但是子線程完全受主線程控制,且不得操作DOM。所以,這個(gè)新標(biāo)準(zhǔn)并沒(méi)有改變JavaScript單線程的本質(zhì)。
2. 任務(wù)隊(duì)列 event loop
單線程就意味著,所有任務(wù)需要排隊(duì),前一個(gè)任務(wù)結(jié)束,才會(huì)執(zhí)行后一個(gè)任務(wù)。如果前一個(gè)任務(wù)耗時(shí)很長(zhǎng),后一個(gè)任務(wù)就不得不一直等著。
于是,所有任務(wù)可以分成兩種,一種是同步任務(wù)(synchronous),另一種是異步任務(wù)(asynchronous)。同步任務(wù)指的是,在主線程上排隊(duì)執(zhí)行的任務(wù),只有前一個(gè)任務(wù)執(zhí)行完畢,才能執(zhí)行后一個(gè)任務(wù);異步任務(wù)指的是,不進(jìn)入主線程、而進(jìn)入"任務(wù)隊(duì)列"(task queue)的任務(wù),只有"任務(wù)隊(duì)列"通知主線程,某個(gè)異步任務(wù)可以執(zhí)行了,該任務(wù)才會(huì)進(jìn)入主線程執(zhí)行。
具體來(lái)說(shuō),異步執(zhí)行的運(yùn)行機(jī)制如下。(同步執(zhí)行也是如此,因?yàn)樗梢员灰暈闆](méi)有異步任務(wù)的異步執(zhí)行。)
所有同步任務(wù)都在主線程上執(zhí)行,形成一個(gè)執(zhí)行棧(execution context stack)。主線程之外,還存在一個(gè)"任務(wù)隊(duì)列"(task queue)。只要異步任務(wù)有了運(yùn)行結(jié)果,就在"任務(wù)隊(duì)列"之中放置一個(gè)事件。一旦"執(zhí)行棧"中的所有同步任務(wù)執(zhí)行完畢,系統(tǒng)就會(huì)讀取"任務(wù)隊(duì)列",看看里面有哪些事件。那些對(duì)應(yīng)的異步任務(wù),于是結(jié)束等待狀態(tài),進(jìn)入執(zhí)行棧,開(kāi)始執(zhí)行。主線程不斷重復(fù)上面的第三步。

只要主線程空了,就會(huì)去讀取"任務(wù)隊(duì)列",這就是JavaScript的運(yùn)行機(jī)制。這個(gè)過(guò)程會(huì)不斷重復(fù)。
3. 定時(shí)器 setTimeout與setInterval
定時(shí)器功能主要由setTimeout()和setInterval()這兩個(gè)函數(shù)來(lái)完成,它們的內(nèi)部運(yùn)行機(jī)制完全一樣,區(qū)別在于前者指定的代碼是一次性執(zhí)行,后者則為反復(fù)執(zhí)行。
setTimeout(fn,0)的含義是,指定某個(gè)任務(wù)在主線程最早可得的空閑時(shí)間執(zhí)行,也就是說(shuō),盡可能早得執(zhí)行。它在"任務(wù)隊(duì)列"的尾部添加一個(gè)事件,因此要等到同步任務(wù)和"任務(wù)隊(duì)列"現(xiàn)有的事件都處理完,才會(huì)得到執(zhí)行。
HTML5標(biāo)準(zhǔn)規(guī)定了setTimeout()的第二個(gè)參數(shù)的最小值(最短間隔),不得低于4毫秒,如果低于這個(gè)值,就會(huì)自動(dòng)增加。在此之前,老版本的瀏覽器都將最短間隔設(shè)為10毫秒。另外,對(duì)于那些DOM的變動(dòng)(尤其是涉及頁(yè)面重新渲染的部分),通常不會(huì)立即執(zhí)行,而是每16毫秒執(zhí)行一次。這時(shí)使用requestAnimationFrame()的效果要好于setTimeout()。
需要注意的是,setTimeout()只是將事件插入了"任務(wù)隊(duì)列",必須等到當(dāng)前代碼(執(zhí)行棧)執(zhí)行完,主線程才會(huì)去執(zhí)行它指定的回調(diào)函數(shù)。要是當(dāng)前代碼耗時(shí)很長(zhǎng),有可能要等很久,所以并沒(méi)有辦法保證,回調(diào)函數(shù)一定會(huì)在setTimeout()指定的時(shí)間執(zhí)行。
4. Node.js的Event Loop
事件輪詢主要是針對(duì)事件隊(duì)列進(jìn)行輪詢,事件生產(chǎn)者將事件排隊(duì)放入隊(duì)列中,隊(duì)列另外一端有一個(gè)線程稱為事件消費(fèi)者會(huì)不斷查詢隊(duì)列中是否有事件,如果有事件,就立即會(huì)執(zhí)行,為了防止執(zhí)行過(guò)程中有堵塞操作影響當(dāng)前線程讀取隊(duì)列,事件消費(fèi)者線程會(huì)委托一個(gè)線程池專門執(zhí)行這些堵塞操作。

Javascript前端和Node.js的機(jī)制類似這個(gè)事件輪詢模型,有的人認(rèn)為Node.js是單線程,也就是事件消費(fèi)者是單線程不斷輪詢,如果有堵塞操作怎么辦,不是堵塞了當(dāng)前單線程的執(zhí)行嗎?
其實(shí)Node.js底層也有一個(gè)線程池,線程池專門用來(lái)執(zhí)行各種堵塞操作,這樣不會(huì)影響單線程這個(gè)主線程進(jìn)行隊(duì)列中事件輪詢和一些任務(wù)執(zhí)行,線程池操作完以后,又會(huì)作為事件生產(chǎn)者將操作結(jié)果放入同一個(gè)隊(duì)列中。
總之,一個(gè)事件輪詢Event Loop需要三個(gè)組件:
事件隊(duì)列Event Queue,屬于FIFO模型,一端推入事件數(shù)據(jù),另外一端拉出事件數(shù)據(jù),兩端只通過(guò)這個(gè)隊(duì)列通訊,屬于一種異步的松耦合。隊(duì)列的讀取輪詢線程,事件的消費(fèi)者,Event Loop的主角。單獨(dú)線程池Thread Pool,專門用來(lái)執(zhí)行長(zhǎng)任務(wù),重任務(wù),干繁重體力活的。
Node.js也是單線程的Event Loop,但是它的運(yùn)行機(jī)制不同于瀏覽器環(huán)境。

根據(jù)上圖,Node.js的運(yùn)行機(jī)制如下。
V8引擎解析JavaScript腳本。解析后的代碼,調(diào)用Node API。 libuv庫(kù)負(fù)責(zé)Node API的執(zhí)行。它將不同的任務(wù)分配給不同的線程,形成一個(gè)Event Loop(事件循環(huán)),以異步的方式將任務(wù)的執(zhí)行結(jié)果返回給V8引擎。 V8引擎再將結(jié)果返回給用戶。
我們可以看到node.js的核心實(shí)際上是libuv這個(gè)庫(kù)。這個(gè)庫(kù)是c寫的,它可以使用多線程技術(shù),而我們的Javascript應(yīng)用是單線程的。
Nodejs 的異步任務(wù)執(zhí)行流程:

用戶寫的代碼是單線程的,但nodejs內(nèi)部并不是單線程!
事件機(jī)制:
Node.js不是用多個(gè)線程為每個(gè)請(qǐng)求執(zhí)行工作的,相反而是它把所有工作添加到一個(gè)事件隊(duì)列中,然后有一個(gè)單獨(dú)線程,來(lái)循環(huán)提取隊(duì)列中的事件。事件循環(huán)線程抓取事件隊(duì)列中最上面的條目,執(zhí)行它,然后抓取下一個(gè)條目。當(dāng)執(zhí)行長(zhǎng)期運(yùn)行或有阻塞I/O的代碼時(shí)
在Node.js中,因?yàn)橹挥幸粋€(gè)單線程不斷地輪詢隊(duì)列中是否有事件,對(duì)于數(shù)據(jù)庫(kù)文件系統(tǒng)等I/O操作,包括HTTP請(qǐng)求等等這些容易堵塞等待的操作,如果也是在這個(gè)單線程中實(shí)現(xiàn),肯定會(huì)堵塞影響其他工作任務(wù)的執(zhí)行,Javascript/Node.js會(huì)委托給底層的線程池執(zhí)行,并會(huì)告訴線程池一個(gè)回調(diào)函數(shù),這樣單線程繼續(xù)執(zhí)行其他事情,當(dāng)這些堵塞操作完成后,其結(jié)果與提供的回調(diào)函數(shù)一起再放入隊(duì)列中,當(dāng)單線程從隊(duì)列中不斷讀取事件,讀取到這些堵塞的操作結(jié)果后,會(huì)將這些操作結(jié)果作為回調(diào)函數(shù)的輸入?yún)?shù),然后激活運(yùn)行回調(diào)函數(shù)。
請(qǐng)注意,Node.js的這個(gè)單線程不只是負(fù)責(zé)讀取隊(duì)列事件,還會(huì)執(zhí)行運(yùn)行回調(diào)函數(shù),這是它區(qū)別于多線程模式的一個(gè)主要特點(diǎn),多線程模式下,單線程只負(fù)責(zé)讀取隊(duì)列事件,不再做其他事情,會(huì)委托其他線程做其他事情,特別是多核的情況下,一個(gè)CPU核負(fù)責(zé)讀取隊(duì)列事件,一個(gè)CPU核負(fù)責(zé)執(zhí)行激活的任務(wù),這種方式最適合很耗費(fèi)CPU計(jì)算的任務(wù)。反過(guò)來(lái),Node..js的執(zhí)行激活任務(wù)也就是回調(diào)函數(shù)中的任務(wù)還是在負(fù)責(zé)輪詢的單線程中執(zhí)行,這就注定了它不能執(zhí)行CPU繁重的任務(wù),比如JSON轉(zhuǎn)換為其他數(shù)據(jù)格式等等,這些任務(wù)會(huì)影響事件輪詢的效率。
5. Nodejs特點(diǎn)

NodeJS的顯著特點(diǎn):異步機(jī)制、事件驅(qū)動(dòng)。
事件輪詢的整個(gè)過(guò)程沒(méi)有阻塞新用戶的連接,也不需要維護(hù)連接?;谶@樣的機(jī)制,理論上陸續(xù)有用戶請(qǐng)求連接,NodeJS都可以進(jìn)行響應(yīng),因此NodeJS能支持比Java、php程序更高的并發(fā)量。
雖然維護(hù)事件隊(duì)列也需要成本,再由于NodeJS是單線程,事件隊(duì)列越長(zhǎng),得到響應(yīng)的時(shí)間就越長(zhǎng),并發(fā)量上去還是會(huì)力不從心。
RESTful API是NodeJS最理想的應(yīng)用場(chǎng)景,可以處理數(shù)萬(wàn)條連接,本身沒(méi)有太多的邏輯,只需要請(qǐng)求API,組織數(shù)據(jù)進(jìn)行返回即可。
6. 實(shí)例
看一個(gè)具體實(shí)例:
console.log('1')
setTimeout(function() {
console.log('2')
new Promise(function(resolve) {
console.log('4')
resolve()
}).then(function() {
console.log('5')
})
setTimeout(() => {
console.log('haha')
})
new Promise(function(resolve) {
console.log('6')
resolve()
}).then(function() {
console.log('66')
})
})
setTimeout(function() {
console.log('hehe')
}, 0)
new Promise(function(resolve) {
console.log('7')
resolve()
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9')
new Promise(function(resolve) {
console.log('11')
resolve()
}).then(function() {
console.log('12')
})
})
new Promise(function(resolve) {
console.log('13')
resolve()
}).then(function() {
console.log('14')
})
// node1 : 1,7,13,8,14,2,4,6,hehe,9,11,5,66,12,haha // 結(jié)果不穩(wěn)定
// node2 : 1,7,13,8,14,2,4,6,hehe,5,66,9,11,12,haha // 結(jié)果不穩(wěn)定
// node3 : 1,7,13,8,14,2,4,6,5,66,hehe,9,11,12,haha // 結(jié)果不穩(wěn)定
// chrome : 1,7,13,8,14,2,4,6,5,66,hehe,9,11,12,haha
chrome的運(yùn)行比較穩(wěn)定,而node環(huán)境下運(yùn)行不穩(wěn)定,可能會(huì)出現(xiàn)兩種情況。
chrome運(yùn)行的結(jié)果的原因是Promise、process.nextTick()的微任務(wù)Event Queue運(yùn)行的權(quán)限比普通宏任務(wù)Event Queue權(quán)限高,如果取事件隊(duì)列中的事件的時(shí)候有微任務(wù),就先執(zhí)行微任務(wù)隊(duì)列里的任務(wù),除非該任務(wù)在下一輪的Event Loop中,微任務(wù)隊(duì)列清空了之后再執(zhí)行宏任務(wù)隊(duì)列里的任務(wù)。
- nodeJs事件循環(huán)運(yùn)行代碼解析
- 帶你了解NodeJS事件循環(huán)
- Nodejs監(jiān)控事件循環(huán)異常示例詳解
- 詳解nodejs異步I/O和事件循環(huán)
- 我的Node.js學(xué)習(xí)之路(三)--node.js作用、回調(diào)、同步和異步代碼 以及事件循環(huán)
- Node.js事件循環(huán)(Event Loop)和線程池詳解
- 深入理解Node.js 事件循環(huán)和回調(diào)函數(shù)
- 小結(jié)Node.js中非阻塞IO和事件循環(huán)
- 深入淺析Node.js 事件循環(huán)
- nodejs?快速入門之事件循環(huán)
相關(guān)文章
JS實(shí)現(xiàn)課程表小程序(仿超級(jí)課程表)加入自定義背景功能
這篇文章主要介紹了JS實(shí)現(xiàn)課程表小程序(仿超級(jí)課程表)加入自定義背景功能,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-12-12
JavaScript編程的10個(gè)實(shí)用小技巧
盡管我使用Javascript來(lái)做開(kāi)發(fā)有很多年了,但它常有一些讓我很驚訝的小特性。對(duì)于我來(lái)說(shuō),Javascript是需要持續(xù)不斷的學(xué)習(xí)的。2014-04-04
JavaScript復(fù)制內(nèi)容到剪貼板的兩種常用方法
最近一個(gè)活動(dòng)頁(yè)面中有一個(gè)小需求,用戶點(diǎn)擊或者長(zhǎng)按就可以復(fù)制內(nèi)容到剪貼板,記錄一下實(shí)現(xiàn)過(guò)程和遇到的坑,需要的朋友可以參考下2018-02-02
window.open打開(kāi)頁(yè)面居中顯示的示例代碼
本篇文章主要是對(duì)window.open打開(kāi)頁(yè)面居中顯示的示例代碼進(jìn)行了介紹,需要的朋友可以過(guò)來(lái)參考下,希望對(duì)大家有所幫助2013-12-12
一文帶你掌握掌握J(rèn)avaScript中不同屬性類型的細(xì)節(jié)
JavaScript是一種功能強(qiáng)大的編程語(yǔ)言,支持面向?qū)ο蟮木幊谭妒?,本文將介紹JavaScript中面向?qū)ο缶幊痰幕靖拍?,包括?duì)象、屬性類型、定義多個(gè)屬性和讀取屬性的特性2023-06-06
javascript簡(jiǎn)單實(shí)現(xiàn)滑動(dòng)菜單效果的方法
這篇文章主要介紹了javascript簡(jiǎn)單實(shí)現(xiàn)滑動(dòng)菜單效果的方法,實(shí)例分析了javascript通過(guò)對(duì)頁(yè)面元素與相關(guān)屬性的操作實(shí)現(xiàn)滑動(dòng)菜單效果的相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-07-07
TypeScript實(shí)現(xiàn)字符串轉(zhuǎn)樹(shù)結(jié)構(gòu)的方法詳解
有一個(gè)多行字符串,每行開(kāi)頭會(huì)用空格來(lái)表示它的層級(jí)關(guān)系,每間隔一層它的空格總數(shù)為2,如何將它轉(zhuǎn)為json格式的樹(shù)型數(shù)據(jù)?本文就跟大家分享下這個(gè)算法2022-09-09
JavaScript中document.forms[0]與getElementByName區(qū)別
在很多情況下JavaScript中document.forms[0]與getElementByName這兩種用法沒(méi)有區(qū)別,這片文章詳細(xì)的解釋了兩者的區(qū)別和用法,有興趣的朋友可以參考一下。2015-01-01
JS表格組件神器bootstrap table詳解(基礎(chǔ)版)
這篇文章主要介紹了JS表格組件神器bootstrap table,bootstrap table界面采用扁平化的風(fēng)格,用戶體驗(yàn)比較好,更好兼容各種客戶端,需要了解更多bootstrap table的朋友可以參考下2015-12-12

