Node.js 的官方文檔中有一段對 Node.js 的簡介,以下。html
Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient.node
大意就是說 Node.js 是基於 V8 的 JavaScript 運行時,事件驅動、非阻塞,所以輕量、高效。git
寥寥數語,並無說清楚 Node.js 究竟是什麼。參考了一些 Node.js 的官方文章以及社區裏的分析,整理以下。github
要想深刻理解 Node.js,咱們須要把 Node.js 進行必要的拆解,瞭解每一個組成部分的做用,它們之間如何交互,最終構成 Node.js 這個強大的運行時環境。npm
上圖是 Node.js 的內部結構圖。咱們能夠看到,自底向上主要能夠分紅三層:最底層是 Node.js 依賴的各類庫,有 V八、libuv 等;中間層是各類 Binding,也就是膠水代碼;最上層是應用代碼,可以使用 Node.js 的各類 API。promise
V8
Google 開源的高性能 JavaScript 引擎,它將 JavaScript 代碼轉換成機器碼,而後執行,所以速度很是快。V8 以 C++ 語言開發,Google 的 Chrome 瀏覽器正是使用的 V8 引擎。瀏覽器
libuv
libuv 以 C 語言開發,內部管理着一個線程池。在此基礎之上,提供事件循環(Event Loop)、異步網絡 I/O、文件系統 I/O等能力。網絡
其餘底層依賴庫
如 c-ares、crypto (OpenSSL)、http-parser 以及 zlib。這些依賴提供了對系統底層功能的訪問,包括網絡、壓縮、加密等。架構
Node.js 底層的依賴庫,有的以 C 語言開發,有的以 C++ 語言開發,如何讓應用代碼(JavaScript)可以與這些底層庫相互調用呢?這就須要中間層的 Binding 來完成。Binding 是一些膠水代碼,可以把不一樣語言綁定在一塊兒使其可以互相溝通。在 Node.js 中,binding 所作的就是把 Node.js 那些用 C/C++ 寫的庫接口暴露給 JS 環境。異步
中間層中,除了 Binding,還有 Addon。Binding 僅橋接 Node.js 核心庫的一些依賴,若是你想在應用程序中包含其餘第三方或者你本身的 C/C++ 庫的話,須要本身完成這部分膠水代碼。你寫的這部分膠水代碼就稱爲 Addon。本質上都是完成橋接的做用,使得應用與底層庫可以互通有無。
應用層的代碼,就沒必要多言了,咱們開發的應用、npm 安裝的包都運行在這裏。
剛接觸 Node.js 的時候,就知道 Node.js 有一個事件循環,相似於 while(true)
,可是不知道每次循環何時開始,何時結束,在每次循環中,Node.js 是如何處理同步與異步代碼的。
要說事件循環,就不得不先說明一下 Node.js 的工做流程。下圖能夠簡要說明。
一個 Node.js 應用啓動時,V8 引擎會執行你寫的應用代碼,保持一份觀察者(註冊在事件上的回調函數)列表。當事件發生時,它的回調函數會被加進一個事件隊列。只要這個隊列還有等待執行的回調函數,事件循環就會持續把回調函數從隊列中拿出並執行。
在回調函數執行過程當中,全部的 I/O 請求都會轉發給工做線程處理。libuv 維持着一個線程池,包含四個工做線程(默認值,可配置)。文件系統 I/O 請求和 DNS 相關請求都會放進這個線程池處理;其餘的請求,如網絡、平臺特性相關的請求會分發給相應的系統處理單元進行處理。
安排給線程池的這些 I/O 操做由 Node.js 的底層庫執行,完成以後觸發相應事件,對應的事件回調函數會被放入事件隊列,等待執行後續操做。這就是一個事件在 Node.js 中執行的整個生命週期。
前面說了,咱們只知道 Node.js 有事件循環,可是不知道每次循環什麼時候開始、什麼時候結束。下面就簡要說明一下每次循環的處理過程,詳細內容請參考Node.js 官方說明。
一次事件循環,大概能夠分爲以下幾個階段:
圖中每個方塊,在事件循環中被稱爲一個階段(phase)。
每一個階段都有本身獨有的一個用於執行回調函數的 FIFO 隊列。當事件循環進入一個指定階段時,會執行隊列中的回調函數,當隊列中已經被清空或者執行的回調函數個數達到系統最大限制時,事件循環會進入下一個階段。
上圖中總共有6個階段:
setTimeout()
和 setInterval()
設置的回調函數。setImmediate()
設置的回調之外的幾乎全部的回調。setImmediate()
設置的回調。socket.on('close', ...)
.這裏有個使人困惑的地方,I/O callbacks
與 poll
這兩個階段有什麼區別? 既然 I/O callbacks
中已經把回調都執行完了,還要 poll
作什麼?
查閱了libuv 的文檔後發現,在 libuv 的 event loop 中,I/O callbacks
階段會執行 Pending callbacks
。絕大多數狀況下,在 poll
階段,全部的 I/O 回調都已經被執行。可是,在某些狀況下,有一些回調會被延遲到下一次循環執行。也就是說,在 I/O callbacks
階段執行的回調函數,是上一次事件循環中被延遲執行的回調函數。
還須要提到的一點是 process.nextTick()
。process.nextTick()
產生的回調函數保存在一個叫作 nextTickQueue
的隊列中,不在上面任何一個階段的隊列裏面。噹噹前操做完成後,nextTickQueue
中的回調函數會當即被執行,無論事件循環處在哪一個階段。也就是說,在 nextTickQueue
中的回調函數被執行完畢以前,事件循環不會往前推動。
以下代碼中使用了 setTimeout()
, setInterval()
, setImmediate()
, promise
, process.nextTick()
,可藉助於輸出結果,理解事件循環。
'use strict'; const fs = require('fs'); console.log('script start'); const interval = setInterval(() => { console.log('setInterval') }, 500); setTimeout(() => { console.log('setTimeout 1'); Promise.resolve().then(() => { console.log('promise 3'); }).then(() => { console.log('promise 4'); process.nextTick(() => { console.log('nextTick 1'); }); }).then(() => { setTimeout(() => { console.log('setTimeout 2'); Promise.resolve().then(() => { console.log('promise 5'); }).then(() => { console.log('promise 6'); process.nextTick(() => { console.log('nextTick 2'); }); }).then(() => { clearInterval(interval); }); }, 0); }); }, 1000); Promise.resolve().then(() => { console.log('promise 1'); }).then(() => { console.log('promise 2'); }); setImmediate(() => { console.log('setImmediate 1'); }); console.log('script done');
執行結果爲:
script start script done promise 1 promise 2 setImmediate 1 setInterval setTimeout 1 promise 3 promise 4 nextTick 1 setInterval setTimeout 2 promise 5 promise 6 nextTick 2