當咱們在接觸學習Node.js的時候,估計咱們看的最多關於Node.js特性的詞是單線程、異步無阻塞、事件驅動。本文經過這幾個特徵詞彙深刻聊聊Node.js的特性。javascript
咱們都知道Node.js的runtime是v8,v8在設計之初是Chrome使用在瀏覽器對JavaScript語言的解析運行引擎,其最大的特色是單線程,而在Node.js對v8的沿用也是針對這一很是重要的特色。什麼是單線程?簡單來講就是一個進程中只有一個線程,程序順序執行,前面執行完成纔會執行後面的程序。來看個Node.js對http服務的模型:html
Node.js的單線程指的是主線程是「單線程」,由主要線程去按照編碼順序一步步執行程序代碼,假如遇到同步代碼阻塞,主線程被佔用,後續的程序代碼執行就會被卡住。實踐一個測試代碼:java
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);
下面爲代碼塊的堆棧圖:c++
JavaScript是解析性語言,代碼按照編碼順序一行一行被壓進stack裏面執行,執行完成後移除而後繼續壓下一行代碼塊進去執行。上面代碼塊的堆棧圖,當主線程接受了request後,程序被壓進同步執行的sleep執行塊(咱們假設這裏就是程序的業務處理),若是在這10s內有第二個request進來就會被壓進stack裏面等待10s執行完成後再進一步處理下一個請求,後面的請求都會被掛起等待前面的同步執行完成後再執行,因此這也說明Node.js單線程的執行模型,由於這樣的特性,咱們的頁面不能有耗時很長的同步處理程序阻塞了程序的後續執行,而對於耗時過長的程序應該採用異步執行,這裏也就是Node.js的第二個特性,異步。segmentfault
咱們平時都會說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 **/
下面是代碼塊的主線程堆棧執行:多線程
看上圖,主線程將main函數壓進stack裏面一行行解析執行,首先遇到setTimeout方法,由於setTimeout是一個異步處理函數,這裏會把setTimeout(callback,timeout),裏面的callback函數移進callback queue裏面,同時會把本身從主線程的stack裏面移除,繼續壓進後面的執行代碼來解析執行,這裏繼續壓進sleep沉睡5s,接下來執行console,等到這裏的同步代碼執行完成後這個時候就會從callback queue(FIFO順序)裏面取回調函數一個個執行。(題外話:就算setTimeout裏面的timeout設置了是0,都是要等待執行塊裏面的同步代碼執行完成後再去執行callback queue裏面的代碼)這就是異步裏面的其一:主線程異步函數處理隊列。框架
一看標題多線程異步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只對文件系統以及DNS實現了多線程I/O封裝,網絡I/O仍是採用單線程形式(可參考libuv設計概述原文:Important libuv uses a thread pool to make asynchronous file I/O operations possible, but network I/O is always performed in a single thread, each loop’s thread.
感謝bd_bai指正)異步
而Node.js的高性能也是得益於其將阻塞的I/O異步化,使得不影響主邏輯的執行。
文章至此,先簡單總結一下Node.js的兩個簡單特性,每一個Node.js進程只有一個主線程在執行程序代碼,在執行的過程當中Node.js將阻塞的I/O異步化,並將其回調函數插入callback queue裏面,等待同步邏輯執行完成後再經過callback queue裏面取出回調函數壓進stack裏面執行。好了,而事件驅動的做用就是取出回調函數。事件驅動又叫事件循環,是指主線程從主線程的異步處理函數隊列裏面不停循環的讀取事件,驅動了全部的異步回調函數的執行。
至此整個Node.js的異步化邏輯能夠不斷循環的跑起來了,以上則是咱們平常所言的Node.js的三大特性以及其原理。