Overview
Node.js 是一個基於 Chrome V8 引擎的JavaScript執行環境(runtime),Node不是一門語言,而是讓js執行在後端的執行時,並且不包括javascript全集,因為在服務端中不包含DOM和BOM. Node也提供了一些新的模組例如http,fs模組等。Node.js 使用了事件驅動、非阻塞式 I/O 的模型,使其輕量又高效並且Node.js 的包管理器 npm,是全球最大的開源庫生態系統。
先來張圖看看node是如何工作的
- 我們寫的js程式碼會交給v8引擎進行處理
- 程式碼中可能會呼叫Node API,Node會交給libuv庫處理
- libuv通過阻塞i/o和多執行緒實現了非同步io
- 通過事件驅動的方式,將結果放到事件佇列中,最終交給我們的應用
Libuv 是 Node.js 關鍵的一個組成部分,它為上層的 Node.js 提供了統一的 API 呼叫,使其不用考慮平臺差距,隱藏了底層實現。它是一個對開發者友好的工具集,包含定時器,非阻塞的網路 I/O,非同步檔案系統訪問,子程式等功能. 它封裝了 Libev、Libeio 以及 IOCP,保證了跨平臺的通用性.所以實際上,Node.js 雖然說是用的 Javascript,但只是在開發時使用 Javascript 的語法來編寫程式。真正的執行過程還是由 V8 將 Javascript 解釋,然後由 C/C++ 來執行真正的系統呼叫,所以並不需要過分擔心 Javascript 執行效率的問題.
上圖涉及到了 Libuv 本身的一個設計理念,事件迴圈(Event Loop)。從這裡,我們可以看到,我們其實對 Node.js 的單執行緒一直有個誤會。事實上,它的單執行緒指的是自身 Javascript 執行環境的單執行緒,Node.js 並沒有給 Javascript 執行時建立新執行緒的能力,最終的實際操作,還是通過 Libuv 以及它的事件迴圈來執行的。這也就是為什麼 Javascript 一個單執行緒的語言,能在 Node.js 裡面實現非同步操作的原因,兩者並不衝突。在任務呼叫中又可分為巨集任務和微任務
macro-task(巨集任務): setTimeout, setInterval, setImmediate, I/O
micro-task(微任務): process.nextTick, 原生Promise(有些實現的promise將then方法放到了巨集任務中), MutationObserver
那巨集任務與微任務對於執行時有什麼影響嗎,在瀏覽器與node中這兩者是有區別的
console.log(1);
setTimeout(function() {
console.log(2);
Promise.resolve(1).then(function() {
console.log('promise');
})
})
setTimeout(function(){
console.log(3);
})
複製程式碼
以上述程式碼為例,在瀏覽器中,先預設走棧 console.log(1), 接著走第一個setTimeout,將promise微任務放到佇列中,執行微任務,微任務執行完再走巨集任務,所以瀏覽器執行時,只要一碰到微任務佇列中有任務就會先去執行微任務再回來執行巨集任務.這裡的輸出結果就會是 1 -> 2 -> promise -> 3
然而在node中,會先將巨集任務佇列中的任務執行完之後再去檢視微任務佇列並執行。這裡的輸出結果就會是 1 -> 2 -> 3 -> promise
REPL
在Node.js中為了使開發者方便測試JavaScript程式碼,提供了一個名為REPL的可互動式執行環境。開發者可以在該執行環境中輸入任何JavaScript表示式,當使用者按下Enter鍵後,REPL執行環境將顯示該表示式的執行結果. 在命令列容器中輸入node命令並按下Enter鍵,即可進入REPL執行環境.
在程式碼中我們也可以使用repl模組來幫我們建立一個repl上下文
let repl = require('repl');
let context = repl.start().context;
context.zfpx = 'zfpx';
context.age = 9;
複製程式碼
repl支援一些基礎命令如下:
- .break 退出當前命令
- .clear 清除REPL執行環境上下文物件中儲存的所有變數與函式
- .exit 退出REPL執行環境
- .save 把輸入的所有表示式儲存到一個檔案中
- .load 把所有的表示式載入到REPL執行環境中
- .help 檢視幫助命令
Console
在Node.js中,使用console物件代表控制檯(在作業系統中表現為一個作業系統指定的字元介面,比如 Window中的命令提示視窗,以下列出一些基本用法:
- console.log
- console.info
- console.error
- console.warn
- console.dir
- console.time
- console.timeEnd
- console.trace
- console.assert
Node Event Loop
┌───────────────────────┐
┌─>│ timers(計時器) │
| | 執行setTimeout以及 |
| | setInterval的回撥。 |
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks |
│ | 處理網路、流、tcp的錯誤 |
| | callback |
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
| | node內部使用 |
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐ ┌───────────────┐
│ │ poll(輪詢) │ │ incoming: │
| | 執行poll中的i/o佇列 | <─────┤ connections, │
| | 檢查定時器是否到時 | │ data, etc. |
│ └──────────┬────────────┘ └───────────────┘
│ ┌──────────┴────────────┐
│ │ check(檢查) │
| | 存放setImmediate回撥 |
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks |
│ 關閉的回撥例如 |
| sockect.on('close') |
└───────────────────────┘
複製程式碼
上面的圖中描述了整個node事件迴圈的流程。可以看到第一張圖中羅列出了多個階段,每個階段維護這一個觀察者佇列
- timers 階段: 這個階段執行setTimeout(callback) 和 setInterval(callback)預定的callback;
- I/O callbacks 階段: 執行除了close事件的callbacks、被timers(定時器,setTimeout、setInterval等)設定的callbacks、setImmediate()設定的callbacks之外的callbacks;
- idle, prepare 階段: 僅node內部使用;
- poll 階段: 獲取新的I/O事件, 適當的條件下node將阻塞在這裡;
- check 階段: 執行setImmediate() 設定的callbacks;
- close callbacks 階段: 比如socket.on(‘close’, callback)的callback會在這個階段執行。
事件迴圈除了維護這些觀察者佇列,還維護了一個 time 欄位,在初始化時會被賦值為0,每次迴圈都會更新這個值。所有與時間相關的操作,都會和這個值進行比較,來決定是否執行。
在圖中,與 timer 相關的過程如下:
更新當前迴圈的 time 欄位,即當前迴圈下的“現在”;
檢查迴圈中是否還有需要處理的任務(handlers/requests),如果沒有就不必迴圈了,即是否 alive。
檢查註冊過的 timer,如果某一個 timer 中指定的時間落後於當前時間了,說明該 timer 已到期,於是執行其對應的回撥函式;
執行一次 I/O polling(即阻塞住執行緒,等待 I/O 事件發生),如果在下一個 timer 到期時還沒有任何 I/O 完成,則停止等待,執行下一個 timer 的回撥。如果發生了 I/O 事件,則執行對應的回撥;由於執行回撥的時間裡可能又有 timer 到期了,這裡要再次檢查 timer 並執行回撥。
process.nextTick
process.nextTick方法屬於微任務,它指定的任務總是發生在所有非同步任務之前。
function Fn() {
this.arrs;
process.nextTick(() => {
this.arrs();
})
}
Fn.prototype.then = function() {
this.arrs = function() { console.log(1); }
}
let fn = new Fn();
fn.then();
複製程式碼
setTimeout 和 setImmediate
setImmediate在poll階段完成時執行,即check階段
setTimeout在poll階段為空閒時,且設定時間到達後執行, 但其在timer階段執行
其二者的呼叫順序取決於當前event loop的上下文,如果他們在非同步i/o callback之外呼叫,其執行先後順序是不確定的。
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
複製程式碼
這是因為後一個事件進入的時候,事件環可能處於不同的階段導致結果的不確定。當我們給了事件環確定的上下文,事件的先後就能確定了。
var fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0)
setImmediate(() => {
console.log('immediate');
})
})
$ node timeout_vs_immediate.js
immediate
timeout
複製程式碼
這是因為fs.readFile的callback執行完後,程式設定了timer 和 setImmediate,因此poll階段不會被阻塞進而進入check階段先執行setImmediate,後進入timer階段執行setTimeout。
Debugger
V8 提供了一個強大的偵錯程式,可以通過 TCP 協議從外部訪問。Nodejs提供了一個內建偵錯程式來幫助開發者除錯應用程式。想要開啟偵錯程式我們需要在程式碼中加入debugger標籤,當Nodejs執行到debugger標籤時會自動暫停(debugger標籤相當於在程式碼中開啟一個斷點)
node inspect main.js
複製程式碼
當然現在更流行的方式是在瀏覽器中進行除錯,node瀏覽器除錯可以通過chrome瀏覽器進行除錯
node --inspect-brk main.js
複製程式碼
開啟chrome 訪問 chrome://inspect即可開始除錯
另外各個編輯器也會有各自的方法可以配置自己的偵錯程式來做除錯