如果我們平常有瀏覽有關(guān)Node.js的文章,估計(jì)我們都會聽到最多關(guān)于Node.js是異步非阻塞I/O,單線程,事件機(jī)制。本章節(jié)主要去深入探討這幾種特性(PS:本文是對所學(xué)知識的學(xué)習(xí)和總結(jié),可能存在理解錯(cuò)誤。請帶著懷疑的眼光,同時(shí)如果有錯(cuò)誤希望能指出。)

一、Node.js的異步I/O,非阻塞I/O
首先我們先來理解幾個(gè)概念:阻塞IO(blocking I/O)和非阻塞IO(non-blocking I/O)、同步IO(synchronous I/O)和異步IO(synchronous I/O)。
問題:這里肯定有人想問,異步I/O和非阻塞I/O不是一回事嗎??
答案:異步I/O和非阻塞I/O根本不是同一回事,曾經(jīng)筆者一直天真的以為非阻塞I/O就是異步I/O T_T(直到看見樸靈大神的深入淺出Node.js)。
1.現(xiàn)在我們來了解什么是異步I/O,什么是同步I/O?
這里轉(zhuǎn)自有趣的知乎er
老張愛喝茶,廢話不說,煮開水。出場人物:老張,水壺兩把(普通水壺,簡稱水壺;會響的水壺,簡稱響水壺)。
1:老張把水壺放到火上,立等水開。(同步阻塞)老張覺得自己有點(diǎn)傻
2: 老張把水壺放到火上,去客廳看電視,時(shí)不時(shí)去廚房看看水開沒有。(同步非阻塞)老張還是覺得自己有點(diǎn)傻,于是變高端了,買了把會響笛的那種水壺。水開之后,能大聲發(fā)出嘀~~~~的噪音。
3:老張把響水壺放到火上,立等水開。(異步阻塞)老張覺得這樣傻等意義不大
4: 老張把響水壺放到火上,去客廳看電視,水壺響之前不再去看它了,響了再去拿壺。(異步非阻塞)老張覺得自己聰明了。
所謂同步異步,只是對于水壺而言。普通水壺,同步;響水壺,異步。雖然都能干活,但響水壺可以在自己完工之后,提示老張水開了。這是普通水壺所不能及的。同步只能讓調(diào)用者去輪詢自己(情況2中),造成老張效率的低下。
所謂阻塞非阻塞,僅僅對于老張而言。立等的老張,阻塞;看電視的老張,非阻塞。情況1和情況3中老張就是阻塞的,媳婦喊他都不知道。雖然3中響水壺是異步的,可對于立等的老張沒有太大的意義。所以一般異步是配合非阻塞使用的,這樣才能發(fā)揮異步的效用。
作者:愚抄
鏈接:https://www.zhihu.com/question/19732473/answer/23434554
來源:知乎
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處。
二、單線程
在專題一的介紹,我們指導(dǎo)Node.js的runtime是V8,而V8設(shè)計(jì)是為了讓Chrome瀏覽器對Javascript語言進(jìn)行編譯和解析的,另外有過JS經(jīng)驗(yàn)的工程師都知道,JS的最大特點(diǎn)是單線程,而Node.js對V8的延用也是針對這一非常重要的特點(diǎn)。那么什么是單線程,單線程指的是,一個(gè)進(jìn)程只能擁有一個(gè)線程,程序按順序執(zhí)行,只有等到前面的程序執(zhí)行完畢,才能執(zhí)行下一個(gè)!來看看Node對Http服務(wù)的模型,

Node.js單線程指的是主線程是單線程,由主線程按照編碼順序一步步執(zhí)行,當(dāng)主線程遇到阻塞的時(shí)候,后續(xù)的程序就會被卡住無法執(zhí)行。說了這么多,不如來實(shí)踐一下驗(yàn)真假:

先將index.js的代碼改成這樣,然后打開瀏覽器,你會發(fā)現(xiàn)瀏覽器在10秒之后才做出反應(yīng),打出Hello Node.js

JavaScript是解析性語言,代碼按照編碼順序一行一行被壓進(jìn)stack里面執(zhí)行,執(zhí)行完成后移除然后繼續(xù)壓下一行代碼塊進(jìn)去執(zhí)行。上面代碼塊的堆棧圖,當(dāng)主線程接受了request后,程序被壓進(jìn)同步執(zhí)行的sleep執(zhí)行塊(我們假設(shè)這里就是程序的業(yè)務(wù)處理),如果在這10s內(nèi)有第二個(gè)request進(jìn)來就會被壓進(jìn)stack里面等待10s執(zhí)行完成后再進(jìn)一步處理下一個(gè)請求,后面的請求都會被掛起等待前面的同步執(zhí)行完成后再執(zhí)行,所以這也說明Node.js單線程的執(zhí)行模型,因?yàn)檫@樣的特性,我們的頁面不能有耗時(shí)很長的同步處理程序阻塞了程序的后續(xù)執(zhí)行,而對于耗時(shí)過長的程序應(yīng)該采用異步執(zhí)行,這里也就是Node.js的第二個(gè)特性,異步。
三、異步
我們平時(shí)說的Node.js是異步的,那么具體是指那部分異步,而答案是主線程的異步處理函數(shù)隊(duì)列+多線程異步I/O
1.主線程的異步處理函數(shù)隊(duì)列
上面那句話看上去是不是有點(diǎn)抽象難懂,現(xiàn)在來進(jìn)行解釋,所謂主線程異步處理函數(shù)隊(duì)列是指主線程的主要執(zhí)行空間除了stack(執(zhí)行棧)和head(產(chǎn)生堆)外,還會callback queue(回調(diào)函數(shù)隊(duì)列),而callback queue是存放了異步處理的回調(diào)函數(shù),當(dāng)一個(gè)異步I/O處理完,就會向callback queue存放一個(gè)回調(diào)函數(shù),當(dāng)stack里面的程序執(zhí)行完,主線程就會從callback queue取出已經(jīng)存放好的回調(diào)函數(shù)去執(zhí)行,而我們平時(shí)最常見的異步,除了事件外,還有timer,例如setTimeout,不如我們舉一個(gè)栗子~
let sleep = (time)=>{
let exit = Date.now+time*1000;
while(Date.now()<exit){};
console.log('end sleep')
return
}
let main = ()=>{
setTimeout(()=>{
console.log('setTimeout run');
},0)
sleep(5);
console.log('after sleep');
}
main()
執(zhí)行輸出:
end sleep
after sleep
undefined(由于每個(gè)函數(shù)執(zhí)行完都會自行return,如果沒指定,就會輸出undefined,這個(gè)是main執(zhí)行完return出來的)
setTimeout run
下面是代碼塊的主線程堆棧執(zhí)行:

看上圖,主線程將main函數(shù)壓進(jìn)stack里面一行行解析執(zhí)行,首先遇到setTimeout方法,因?yàn)閟etTimeout是一個(gè)異步處理函數(shù),這里會setTimeout(callback,timeout),里面的callback函數(shù)移進(jìn)callback queue里面,同時(shí)會把自己從主線程的stack里面移除,繼續(xù)壓進(jìn)后面的執(zhí)行代碼來解析執(zhí)行,這里繼續(xù)壓進(jìn)sleep沉睡5s,接下來執(zhí)行console,等到這里的同步代碼執(zhí)行完成后這個(gè)時(shí)候就會從callback queue里面取回調(diào)函數(shù)一個(gè)個(gè)執(zhí)行。(題外話:就算setTimeout里面的timeout設(shè)置了是0,都是要等待執(zhí)行塊里面的同步代碼執(zhí)行完成后再去執(zhí)行callback queue里面的代碼)這就是異步里面的其一:主線程異步函數(shù)處理隊(duì)列。(PS,setTimeout的回調(diào)函數(shù)的執(zhí)行時(shí)間不是當(dāng)前隊(duì)列,而是下一個(gè)執(zhí)行隊(duì)列,即是是設(shè)置為,也最多是下一次執(zhí)行隊(duì)列第一個(gè)執(zhí)行,詳情阮一峰JS運(yùn)行機(jī)制)
2.多線程異步I/O
這里可能有人會有疑問,買賣皮喔!你不是說Node.js是單線程嗎,你這不是自己打臉嗎?我想說,這里其實(shí)是沒有沖突的,Node.js每個(gè)進(jìn)程里面只有一個(gè)主線程來處理程序。因此,主線程是單線程,而主線程之外調(diào)用的I/O處理是通過一個(gè)叫做線程池的結(jié)構(gòu)來管理的,所以I/O的處理是多線程的,而主線程和I/O線程池則通過上面剛剛講述的主線程的異步處理函數(shù)隊(duì)列來協(xié)作。(PSNode.js只對文件系統(tǒng)以及DNS實(shí)現(xiàn)了多線程I/O封裝,網(wǎng)絡(luò)I/O還是采用單線程形式)如圖:

上圖在主線程中,當(dāng)遇到需要處理的I/O時(shí),就將I/O的處理放在I/O線程池中管理,而主線程繼續(xù)執(zhí)行,當(dāng)I/O線程池中有I/O完成了,就會想callback queue注冊回調(diào)函數(shù)等待主線程執(zhí)行,而Node.js的高性能也是得益于其將阻塞的I/O異步化,使得不影響主邏輯的執(zhí)行。
四、事件驅(qū)動
文章至此,我們先進(jìn)行總結(jié),Node.js至此我們簡介了兩個(gè)主要特性,單線程,異步,每個(gè)Node程序只會在主線程中執(zhí)行程序代碼,在執(zhí)行過程中將阻塞的I/O操作異步化,將放至I/O線程池中進(jìn)行管理,當(dāng)線程池中有I/O操作完成,就會向callback queue注冊回調(diào)函數(shù),等待同步邏輯執(zhí)行完成后再通過callback queue里面取出回調(diào)函數(shù)壓進(jìn)stack里面執(zhí)行,好了,而事件驅(qū)動的作用就是取出回調(diào)函數(shù)。事件驅(qū)動又叫事件循環(huán),是指主線程從主線程的異步處理函數(shù)隊(duì)列里面不停循環(huán)的讀取事件,驅(qū)動了所有的異步回調(diào)函數(shù)的執(zhí)行。詳情Node事件輪詢

至此整個(gè)Node.js的異步化邏輯可以不斷循環(huán)的跑起來了,以上則是我們?nèi)粘K缘腘ode.js的三大特性以及其原理。
參考資料:
Node事件輪詢
JS運(yùn)行機(jī)制
Node.js特性分析
非阻塞I/O和異步I/O的區(qū)別