文章原文: https://yq.aliyun.com/article...
本文相對於原文有部分修改node
Node.js以高效,輕量著稱,具備非阻塞I/O,事件驅動的特性.
非阻塞I/O很淺顯的解釋就是: 代碼以單線程的方式執行,在遇到I/O操做時Node會開闢新的線程去執行I/O操做,主線程代碼繼續執行.
事件驅動很淺顯的解釋就是: 事件產生者發佈一個事件,事件訂閱者在收到事件後執行某段代碼.
但非阻塞I/O,事件驅動究竟是如何實現的呢,它們跟Node.js的單線程有什麼關係呢?git
C/C++底層:github
Libuv(docs,GitHub)是Node.js關鍵的一個組成部分,它爲上層js提供了統一的API調用,兼容了平臺差別,隱藏了底層實現(它來源於libev,然而libev只能運行於Unix-like系統上。爲了可以使Node.js運行在Windows/Unix-like系統上,libuv所以產生了)網絡
Network I/O: 網絡I/O異步
舉一個文件操做的例子來闡述Node.js整個的執行流程async
const fs = require("fs") fs.open("./test.txt", "w", (err, data) => { // TODO });
整個代碼的調用過程大體可描述爲: lib/fs.js -> src/node_file.cc -> uv_fs
tcp
具體來講,當咱們調用 fs.open
時,Node.js 經過 process.binding
調用 C/C++ 層面的 Open 函數,而後經過它調用 Libuv 中的具體方法 uv_fs_open
,最後執行的結果經過回調的方式傳回,完成流程。在圖中,能夠看到平臺判斷的流程,須要說明的是,這一步是在編譯的時候已經決定好的,並非在運行時中。函數
整體來講,咱們在 Javascript 中調用的方法,最終都會經過 process.binding
傳遞到 C/C++ 層面,最終由他們來執行真正的操做。Node.js 即這樣與操做系統進行互動。工具
經過這個過程,咱們能夠發現,實際上,Node.js 雖說是用的 Javascript,但只是在開發時使用 Javascript 的語法來編寫程序。真正的執行過程仍是由 V8 將 Javascript 解釋,而後由 C/C++ 來執行真正的系統調用,因此並不須要過度擔憂 Javascript 執行效率的問題。能夠看出,Node.js 並非一門語言,而是一個平臺.oop
根據上文的鋪墊,咱們能夠知道真正執行系統操做的是Libuv層,Libuv自己就是異步和事件驅動的,因此,當咱們調用I/O操做時,Libuv開啓線程來執行此次I/O操做,執行完成後傳回給JavaScript進行後續操做.
這裏的I/O包括了文件I/O和網絡I/O,這二者的實現並不相同,文件I/O和DNS等操做都是依託線程池(Thread Pool)來實現,而網絡I/O(包括TCP,UDP,TTY等)是由epoll,IOCP,kqueue來實現的.
一個異步I/O的流程大致以下:
發起I/O調用
執行回調
從這裏,咱們能夠看到,咱們其實對 Node.js 的單線程一直有個誤會。事實上,它的單線程指的是自身 Javascript 運行環境的單線程,Node.js 並無給 Javascript 執行時建立新線程的能力,最終的實際操做,仍是經過 Libuv 以及它的事件循環來執行的。這也就是爲何 Javascript 一個單線程的語言,能在 Node.js 裏面實現異步操做的緣由,二者並不衝突。
當咱們寫了一大堆事件處理函數後,Libuv 如何來執行這些回調呢?這就提到了咱們以前說到的 uv_run,先看一張它的執行流程圖:
在 uv_run
函數中,會維護一系列的監視器(觀察者隊列):
typedef struct uv_loop_s uv_loop_t; typedef struct uv_err_s uv_err_t; typedef struct uv_handle_s uv_handle_t; typedef struct uv_stream_s uv_stream_t; typedef struct uv_tcp_s uv_tcp_t; typedef struct uv_udp_s uv_udp_t; typedef struct uv_pipe_s uv_pipe_t; typedef struct uv_tty_s uv_tty_t; typedef struct uv_poll_s uv_poll_t; typedef struct uv_timer_s uv_timer_t; typedef struct uv_prepare_s uv_prepare_t; typedef struct uv_check_s uv_check_t; typedef struct uv_idle_s uv_idle_t; typedef struct uv_async_s uv_async_t; typedef struct uv_process_s uv_process_t; typedef struct uv_fs_event_s uv_fs_event_t; typedef struct uv_fs_poll_s uv_fs_poll_t; typedef struct uv_signal_s uv_signal_t;
這些監視器都有對應着一種異步操做,它們經過 uv_TYPE_start
,來註冊事件監聽以及相應的回調。
在 uv_run
執行過程當中,它會不斷的檢查這些隊列中是或有 pending
狀態的事件,有則觸發,並且它在這裏只會執行一個回調,避免在多個回調調用時發生競爭關係,由於 Javascript 是單線程的,沒法處理這種狀況。
上面的圖中,對 I/O 操做的事件驅動,表達的比較清楚。除了咱們常提到的 I/O 操做,圖中還表述了一種狀況,timer(定時器)。它與其餘二者不一樣之處在於,它沒有單獨開立新的線程,而是在事件循環中直接完成的。
事件循環除了維護那些觀察者隊列,還維護了一個 time
字段,在初始化時會被賦值爲0,每次循環都會更新這個值。全部與時間相關的操做,都會和這個值進行比較,來決定是否執行。
在圖中,與 timer
相關的過程以下: