Node.js之深入理解特性
前言
當我們在接觸學習Node.js的時候,估計我們看的最多關于Node.js特性的詞是 單線程 、 異步無阻塞 、 事件驅動 。本文通過這幾個特征詞匯深入聊聊Node.js的特性。
單線程
我們都知道Node.js的runtime是v8,v8在設計之初是Chrome使用在瀏覽器對JavaScript語言的解析運行引擎,其最大的特點是單線程,而在Node.js對v8的沿用也是針對這一非常重要的特點。什么是單線程?簡單來說就是一個進程中只有一個線程,程序順序執行,前面執行完成才會執行后面的程序。來看個Node.js對http服務的模型:
Node.js的單線程指的是主線程是“單線程”,由主要線程去按照編碼順序一步步執行程序代碼,假如遇到同步代碼阻塞,主線程被占用,后續的程序代碼執行就會被卡住。實踐一個測試代碼:
var http = require('http');
function sleep(time) {
var _exit = Date.now() + time * 1000;
while( Date.now() < _exit ) {}
return ;
}
var server = http.createServer(function(req, res){
sleep(10);
res.end('server sleep 10s');
});
server.listen(8080);</code></pre>
下面為代碼塊的堆棧圖:

JavaScript是解析性語言,代碼按照編碼順序一行一行被壓進stack里面執行,執行完成后移除然后繼續壓下一行代碼塊進去執行。上面代碼塊的堆棧圖,當主線程接受了request后,程序被壓進同步執行的sleep執行塊(我們假設這里就是程序的業務處理),如果在這10s內有第二個request進來就會被壓進stack里面等待10s執行完成后再進一步處理下一個請求,后面的請求都會被掛起等待前面的同步執行完成后再執行,所以這也說明Node.js單線程的執行模型,因為這樣的特性, 我們的頁面不能有耗時很長的同步處理程序阻塞了程序的后續執行,而對于耗時過長的程序應該采用異步執行 ,這里也就是Node.js的第二個特性,異步。
異步
我們平時都會說Node.js是異步,但是所說的異步具體指的是什么異步?更進一步的說應該是 主線程的異步處理函數隊列+多線程異步I/O 。
主線程的異步處理函數隊列
首先,所謂的主線程的異步處理函數隊列指的是主線程的主要執行空間除了stack以及heap外,還有callback queue(回調函數隊列),而callback queue是存放了異步處理的回調函數,在一個執行塊里面,當里面的同步代碼執行完成后,會從callback queue里面取出回調函數一個個執行,我們最常見的異步處理函數就是 setTimeout ,一個簡單的例子來講述:
function sleep(time) {
var _exit = Date.now() + time * 1000;
while( Date.now() < _exit ) {}
return ;
}
function main(){
setTimeout(function(){
console.log('setTimeout run');
},0);
sleep(5);
console.log('after sleep');
}
main();
/ 執行輸出
after sleep
setTimeout run/</code></pre>
下面是代碼塊的主線程堆棧執行:

看上圖,主線程將main函數壓進stack里面一行行解析執行,首先遇到setTimeout方法,因為setTimeout是一個異步處理函數,這里會把setTimeout(callback,timeout),里面的callback函數移進callback queue里面,同時會把自己從主線程的stack里面移除,繼續壓進后面的執行代碼來解析執行,這里繼續壓進sleep沉睡5s,接下來執行console,等到這里的同步代碼執行完成后這個時候就會從callback queue(FIFO順序)里面取回調函數一個個執行。 (題外話:就算setTimeout里面的timeout設置了是0,都是要等待執行塊里面的同步代碼執行完成后再去執行callback queue里面的代碼) 這就是異步里面的其一:主線程異步函數處理隊列。
多線程異步I/O
一看標題多線程異步I/O可能會有疑問,不是說Node.js是單線程的嗎?其實這里并沒有沖突,Node.js每個進程里面只有一個主線程來處理程序,所以,主線程是單線程的。 而主線程之外調用的I/O處理是通過一個叫做線程池來管理和分配線程來處理I/O,所以對I/O的處理是多線程 。而主線程和I/O線程池則通過上面剛剛講述的主線程的異步處理函數隊列來協作。除了上文所說的timers模塊里面的 setTimeout 函數外,Node.js還對文件系統、網絡都實現了異步化調用(題外話:系統底層的I/O異步化都是基于c++的異步框架libuv來實現,然后往上層提供JavaScript調用接口)

而Node.js的高性能也是得益于其將阻塞的I/O異步化,使得不影響主邏輯的執行。
事件驅動
文章至此,先簡單總結一下Node.js的兩個簡單特性, 每個Node.js進程只有一個主線程在執行程序代碼,在執行的過程中Node.js將阻塞的I/O異步化,并將其回調函數插入callback queue里面,等待同步邏輯執行完成后再通過callback queue里面取出回調函數壓進stack里面執行 。好了,而事件驅動的作用就是 取出回調函數 。事件驅動又叫事件循環,是指主線程從主線程的異步處理函數隊列里面不停循環的讀取事件,驅動了所有的異步回調函數的執行。

至此整個Node.js的異步化邏輯可以不斷循環的跑起來了,以上則是我們日常所言的Node.js的三大特性以及其原理。
來自:https://segmentfault.com/a/1190000008961775