在上一篇文章理解NodeJs中的Event Loop、Timers以及process.nextTick中筆者提了幾個問題,如今針對這些問題給出個人理解,若有錯漏煩請指正。html
若是你對NodeJs系列感興趣,歡迎關注前端神盾局或筆者微信(w979436427)交流討論node學習心得前端
在上一篇文章中提到在poll階段會「接收新的I/O事件而且在適當時node會阻塞在這裏」,那什麼狀況下會阻塞呢?阻塞多久呢?node
對於這個問題,咱們必須深刻到libuv的源碼,看看poll階段是怎麼實現的:c++
int uv_run(uv_loop_t* loop, uv_run_mode mode) {
int timeout;
int r;
int ran_pending;
r = uv__loop_alive(loop);
if (!r)
uv__update_time(loop);
while (r != 0 && loop->stop_flag == 0) {
uv__update_time(loop);
uv__run_timers(loop);
ran_pending = uv__run_pending(loop);
uv__run_idle(loop);
uv__run_prepare(loop);
timeout = 0;
if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
timeout = uv_backend_timeout(loop);
// 這是poll階段
uv__io_poll(loop, timeout);
uv__run_check(loop);
uv__run_closing_handles(loop);
if (mode == UV_RUN_ONCE) {
/* UV_RUN_ONCE implies forward progress: at least one callback must have * been invoked when it returns. uv__io_poll() can return without doing * I/O (meaning: no callbacks) when its timeout expires - which means we * have pending timers that satisfy the forward progress constraint. * * UV_RUN_NOWAIT makes no guarantees about progress so it's omitted from * the check. */
uv__update_time(loop);
uv__run_timers(loop);
}
r = uv__loop_alive(loop);
if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
break;
}
/* The if statement lets gcc compile it to a conditional store. Avoids * dirtying a cache line. */
if (loop->stop_flag != 0)
loop->stop_flag = 0;
return r;
}
複製代碼
從源碼咱們能夠看到uv__io_poll
傳入了timeout
做爲參數,而這個timeout
就決定了poll階段阻塞的時長,明白這一點咱們就能夠把問題轉化成:是什麼決定的timeout
的值?git
再回到源碼中,timeout
的初始值爲0,也就意味着poll階段以後會直接轉入check階段而不會發生阻塞。可是當(mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT
這些條件成立時,timeout
就由uv_backend_timeout
的返回值決定。github
這裏須要插播一下關於mode
值的問題,根據官方文檔 mode
一共有三種狀況:c#
UV_RUN_DEFAULT
UV_RUN_ONCE
UV_RUN_NOWAIT
這裏咱們只關心UV_RUN_DEFAULT
,由於Node event loop使用的是這種模式.bash
OK~回到問題,咱們再看一下uv_backend_timeout
會返回什麼?微信
int uv_backend_timeout(const uv_loop_t* loop) {
if (loop->stop_flag != 0)
return 0;
if (!uv__has_active_handles(loop) && !uv__has_active_reqs(loop))
return 0;
if (!QUEUE_EMPTY(&loop->idle_handles))
return 0;
if (!QUEUE_EMPTY(&loop->pending_queue))
return 0;
if (loop->closing_handles)
return 0;
return uv__next_timeout(loop);
}
複製代碼
這是一個多步條件判斷函數,咱們一個個分析:異步
uv_stop()
,stop_flag != 0
),timeout
爲0timeout
爲0idle_handles
和pending_queue
,timeout
爲0(對於idle_handles
和pending_queue
分別表明什麼,筆者尚未概念,若是後面有相應資料會及時更新)timeout
爲0uv__next_timeout
處理int uv__next_timeout(const uv_loop_t* loop) {
const struct heap_node* heap_node;
const uv_timer_t* handle;
uint64_t diff;
heap_node = heap_min((const struct heap*) &loop->timer_heap);
if (heap_node == NULL)
return -1; /* block indefinitely */
handle = container_of(heap_node, uv_timer_t, heap_node);
if (handle->timeout <= loop->time)
return 0;
// 這句代碼給出了關鍵性的指導
// 對比當前loop的時間戳
diff = handle->timeout - loop->time;
//不能大於最大的INT_MAX
if (diff > INT_MAX)
diff = INT_MAX;
return diff;
}
複製代碼
總結一下,event loop 知足如下條件時,poll階段會進行阻塞:
而阻塞的時間最長不超過給定定時器的最小閥值
setTimeout
和setImmediate
的執行順序是不必定的?上文提到setTimeout
和setImmediate
在非I/O循環中,執行順序是不必定的,好比:
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
複製代碼
相同代碼,兩次運行結果倒是相反的,這是爲何呢?
在node中,setTimeout(cb, 0) === setTimeout(cb, 1)
在event loop的第一個階段(timers階段),node都會從一堆定時器中取出一個最小閥值的定時器來與loop->time
進行比較,若是閥值小於等於loop->time
表示定時器已超時,相應的回調便會執行(隨後會檢查下一個定時器),若是沒有則會進入下一個階段。
因此setTimeout
是否在第一階段執行取決於loop->time
的大小,這裏可能出現兩種狀況:
因爲第一次loop前的準備耗時超過1ms,當前的loop->time >=1
,則uv_run_timer
生效,timeout
先執行
因爲第一次loop前的準備耗時小於1ms,當前的loop->time < 1
,則本次loop中的第一次uv_run_timer
不生效,那麼io_poll
後先執行uv_run_check
,即immediate
先執行,而後等close cb
執行完後,繼續執行uv_run_timer
這就是爲何同一段代碼,執行結果隨機的緣故。那爲何說在I/O回調中,必定是先immediate
執行呢,其實也很容易理解,考慮如下場景:
// timeout_vs_immediate.js
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
複製代碼
因爲timeout
和immediate
的事件註冊是在readFile
的回調執行時觸發的,因此必然的,在readFile
的回調執行前的每一次event loop進來的uv_run_timer
都不會有超時事件觸發 那麼當readFile
執行完畢,poll階段收到監聽的fd事件完成後,執行了該回調,此時
timeout
事件註冊immediate
事件註冊readFile
的回調執行完畢,那麼就會從uv_io_poll
中出來,此時當即執行uv_run_check
,因此immediate
事件被執行掉uv_run_timer
檢查timeout
事件,執行timeout
事件因此你會發現,在I/O回調中註冊的二者,永遠都是immediately
先執行
棧展開主要是指在拋出異常後逐層匹配catch語句的過程,舉個例子:
function a(){
b();
}
function b(){
c();
}
function c(){
throw new Error('from function c');
}
a();
複製代碼
這個例子中,函數c
拋出異常,這是首先會在c
函數自己檢查是否存在try相關的catch語句,若是沒有就退出當前函數,而且釋放當前函數的內存並銷燬局部對象,繼續到b
函數中查找,這個過程就稱之爲棧展開。