JS是一門單線程的語言,若是沒有異步操做的話,一個很耗時的操做,就能夠堵塞整個進程。而出現異步操做以後,就會有數據通訊之間的問題,而event loop很好的解決了這個問題。html
什麼是Event loop?這是咱們第一個須要知道的問題。 在html官方標準中是這麼介紹的。node
To coordinate events, user interaction, scripts, rendering, networking, and so forth, user agents must use event loops as described in this section. There are two kinds of event loops: those for browsing contexts, and those for workers.web
爲了協調事件,用戶交互,腳本運行,頁面渲染,網絡請求等,用戶代理必須使用本節描述event loop。有兩種event loop,一種是browsing contexts,另外一種是workers.ajax
在標準文檔中能夠看到兩種task,一種就叫task,還有一種叫Microtask。編程
1、taskapi
An event loop has one or more task queues. A task queue is an ordered list of taskspromise
規範中指出一個事件循環有一個或者多個任務,任務被有序的排列在隊列中。這裏咱們列舉幾個典型的任務源:瀏覽器
2、microtaskbash
Each event loop has a microtask queue. A microtask is a task that is originally to be queued on the microtask queue rather than a task queue. There are two kinds of microtasks: solitary callback microtasks, and compound microtasks.網絡
規範中也指出,每個event loop只有一個微任務隊列,微任務一般只排列在微任務隊列上,而不是任務隊列。這裏有兩種微任務:回調微任務和複合微任務。舉幾個典型的微任務:
3、event loop運行機制
在寫這個以前,先寫幾條總結出來的規律:
用僞代碼表示爲:
一個任務,清空微任務棧,一個任務,清空微任務棧,...
關於整個運行過程,能夠參見規範第8章
4、example
// 簡稱set1
setTimeout(()=>{
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
// 簡稱set2
setTimeout(()=>{
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
// 簡稱set3
setTimeout(() => {
console.log('timer3')
}, 0)
}, 0)
Promise.resolve().then(function() {
console.log('promise3')
})
console.log('start')
複製代碼
循環一
一、將腳本任務放入到task隊列。
二、從task中取出一個任務運行,運行的結果是將set1和set2放入到task中,將promise.then放入到microtask中,輸出start。
三、檢查microtask checkpoint,看microtask隊列中是否有任務。
四、運行microtask中全部的任務,輸出promise3。
五、清空microtask隊列以後,進入下一個循環。
循環二
一、從task中在取出一個set1任務,運行的結果是輸出timer1,將promise.then放入到microtask隊列中。
二、檢查microtask checkpoint,看microtask隊列中是否有任務。
三、運行microtask中全部的任務,輸出promise1。
四、清空microtask隊列以後,進入下一個循環。
循環三
一、從task中在取出一個set2任務,運行的結果是輸出timer2,將promise.then放入到microtask隊列中,將set3放入到task隊列中。
二、檢查microtask checkpoint,看microtask隊列中是否有任務。
三、運行microtask中全部的任務,輸出promise2。
四、清空microtask隊列以後,進入下一個循環。
循環四
一、從task中在取出一個set3任務,運行的結果是輸出timer3
二、檢查microtask checkpoint,看microtask隊列中沒有任務,進入下一個循環。
循環五
檢測task隊列和microtask隊列都爲空,WorkerGlobalScope對象中closing標誌位爲true,銷燬event loop。
start
promise3
timer1
promise1
timer2
promise2
timer3
複製代碼
咱們先來看一下node的架構。
node的異步是經過底層的libuv來實現的。
1、libuv是什麼
libuv enforces an asynchronous, event-driven style of programming. Its core job is to provide an event loop and callback based notifications of I/O and other activities. libuv offers core utilities like timers, non-blocking networking support, asynchronous file system access, child processes and more.
libuv使用異步和事件驅動的編程風格。它的核心工做是提供一個event-loop,還有基於I/O和其它事件通知的回調函數。libuv還提供了一些核心工具,例如定時器,非阻塞的網絡支持,異步文件系統訪問,子進程等。
2、libuv中的event loop
在node的官方doc中,將El分紅了六個階段,咱們能夠看一下下面的圖:
當node開始運行的時候,它會初始化一個event loop,而每一個event loop都包含如下六個階段:
每個階段都有一個回調的FIFO隊列,當EL運行到一個指定階段的時候,node將會執行這個隊列,當隊列中全部的回調都執行完或者執行的回調數上限的時候,EL會跳到下一個階段。以上全部階段不包含process.nextTick()。
整個的EL運行過程源碼註釋版:
//deps/uv/src/unix/core.c
int uv_run(uv_loop_t *loop, uv_run_mode mode) {
int timeout;
int r;
int ran_pending;
//uv__loop_alive返回的是event loop中是否還有待處理的handle或者request
//以及closing_handles是否爲NULL,若是均沒有,則返回0
r = uv__loop_alive(loop);
//更新當前event loop的時間戳,單位是ms
if (!r)
uv__update_time(loop);
while (r != 0 && loop->stop_flag == 0) {
//使用Linux下的高精度Timer hrtime更新loop->time,即event loop的時間戳
uv__update_time(loop);
//執行判斷當前loop->time下有無到期的Timer,顯然在同一個loop裏面timer擁有最高的優先級
uv__run_timers(loop);
//判斷當前的pending_queue是否有事件待處理,而且一次將&loop->pending_queue中的uv__io_t對應的cb所有拿出來執行
ran_pending = uv__run_pending(loop);
//實如今loop-watcher.c文件中,一次將&loop->idle_handles中的idle_cd所有執行完畢(若是存在的話)
uv__run_idle(loop);
//實如今loop-watcher.c文件中,一次將&loop->prepare_handles中的prepare_cb所有執行完畢(若是存在的話)
uv__run_prepare(loop);
timeout = 0;
//若是是UV_RUN_ONCE的模式,而且pending_queue隊列爲空,或者採用UV_RUN_DEFAULT(在一個loop中處理全部事件),則將timeout參數置爲
//最近的一個定時器的超時時間,防止在uv_io_poll中阻塞住沒法進入超時的timer中
if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
timeout = uv_backend_timeout(loop);
//進入I/O處理的函數(重點分析的部分),此處掛載timeout是爲了防止在uv_io_poll中陷入阻塞沒法執行timers;而且對於mode爲
//UV_RUN_NOWAIT類型的uv_run執行,timeout爲0能夠保證其當即跳出uv__io_poll,達到了非阻塞調用的效果
uv__io_poll(loop, timeout);
//實如今loop-watcher.c文件中,一次將&loop->check_handles中的check_cb所有執行完畢(若是存在的話)
uv__run_check(loop);
//執行結束時的資源釋放,loop->closing_handles指針指向NULL
uv__run_closing_handles(loop);
if (mode == UV_RUN_ONCE) {
//若是是UV_RUN_ONCE模式,繼續更新當前event loop的時間戳
uv__update_time(loop);
//執行timers,判斷是否有已經到期的timer
uv__run_timers(loop);
}
r = uv__loop_alive(loop);
//在UV_RUN_ONCE和UV_RUN_NOWAIT模式中,跳出當前的循環
if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
break;
}
//標記當前的stop_flag爲0,表示當前的loop執行完畢
if (loop->stop_flag != 0)
loop->stop_flag = 0;
//返回r的值
return r;
}
複製代碼
能夠結合上面的六個過程看一下。
3、poll階段
在進入poll階段以前,會先對timeout進行處理
timeout = 0;
if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
timeout = uv_backend_timeout(loop);
uv__io_poll(loop, timeout);
複製代碼
timeout做爲uv__io_poll
的第二個參數,當timeout等於0的時候會跳過poll階段。
咱們能夠看一下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);
}
複製代碼
以上五種狀況(退出事件循環,沒有任何異步任務,idle_handles和pending_queue不爲空,循環進入到closing_handles)返回的timeout都爲0。
uv__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;
//這句代碼給出了關鍵性的指導
diff = handle->timeout - loop->time;
//不能大於最大的INT_MAX
if (diff > INT_MAX)
diff = INT_MAX;
return diff;
}
複製代碼
diff表明的是,距離最近的一個異步回調的時間。最大是32767微秒。而後將diff做爲timeout的值,傳遞給poll階段。
poll階段主要有兩個功能: 一、計算poll階段堵塞和輪詢還有多長時間。 二、處理poll階段中的事件。
當EL進入到poll階段的時候,若是代碼中沒有設定的timers,那麼會發生如下兩種狀況:
若是poll隊列不是空的,將執行poll階段裏面的cb,直到cb爲空,或者執行的cb達到上限。
若是poll爲空的狀況,又會有兩種狀況發生:
一旦poll階段是空的,EL會檢查是否有到期的timers,若是有一個或者多個已經到達,那麼會直接跳到timers階段執行timers的回調。
用一張圖表示:
4、setImmediate() vs setTimeout()
二者的用法是類似的,而setImmediate進入的是check階段,而setTimeout進入的是timer的階段。
而在node的docs中舉了個例子,以下:
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
複製代碼
而在屢次執行中,二者的觸發順序不必定相同:
$ node timeout_vs_immediate.js
timeout
immediate
$ node timeout_vs_immediate.js
immediate
timeout
複製代碼
而將其放在i/o中執行,二者的順序是固定的:
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
複製代碼
輸出的結果:
$ node timeout_vs_immediate.js
immediate
timeout
$ node timeout_vs_immediate.js
immediate
timeout
複製代碼
在node中,計時器的時間是精確到毫秒級別的,因此setTimeout(cb, 0) === setTimeout(cb, 1)。 EL初始化是須要耗時的,可是hrtime這個值精確到納秒級別,因此整個腳本運行會發生如下兩種狀況:
一、loop準備時間超過1ms,那麼loop->time >=1,就會發生
uv_run_timers
。 二、loop準備時間小於1ms,那麼loop->time<1,uv_run_timers
不生效,就會直接到後面的check階段去。
而若是有fs的狀況下,直接走的是uv__io_poll
,觸發回調以後,直接走check,在走timer階段。
5、process.nextTick()
process.nextTick()在node中不參與任何階段,可是每當切換階段的時候,須要清空process.nextTick()隊列中的回調。
看一個例子:
var fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
process.nextTick(()=>{
console.log('nextTick3');
})
});
process.nextTick(()=>{
console.log('nextTick1');
})
process.nextTick(()=>{
console.log('nextTick2');
})
});
複製代碼
輸出結果:
nextTick1
nextTick2
setImmediate
nextTick3
setTimeout
複製代碼
整個循環過程: 循環一:
一、進來的時候,直接進入poll階段,執行回調。
二、掛載setTimeout,掛載setImmediate,將process.nextTick推動nextTick隊列中
三、先執行nextTick隊列,輸出nextTick1和nextTick2。
四、進入check階段,執行setImmediate回調,輸出setImmediate。
五、在執行nextTick隊列,輸出nextTick3。
循環二:
一、進入timer階段,有到期的定時器,輸出setTimeout。