衆所周知,JavaScript
這門語言是單線程的。那也就是說,JavaScript
在同一時間只能作一件事,後面的事情必須等前面的事情作完以後才能獲得執行。javascript
JavaScript
單線程這件事乍一看好像沒毛病,代碼原本就是須要按順序執行的嘛,先來後到,後面的你就先等着。若是是計算量致使的排隊,那沒辦法,老老實實排吧。但若是是由於 I/O
很慢(好比發一個 Ajax
請求,須要 200ms 才能返回結果),那這個等待時間就沒太必要了,徹底能夠先執行後面其餘的任務,等你請求的數據回來了再執行 Ajax
後面的操做嘛。html
由此,
JavaScript
中的任務分紅了兩種,第一種是同步任務,指的是在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務;第二種是異步任務,指的是不進入主線程、而進入「任務隊列」的任務,只有「任務隊列」通知主線程,某個異步任務能夠執行了,該任務纔會進入主線程執行。java
其執行過程以下:node
JavaScript
引擎運行JavaScript
時,有一個主線程和一個任務隊列。- 同步任務跑在主線程上面,異步任務扔進任務隊列中進行等待。
- 主線程中的任務執行完畢以後,回去看看任務隊列中有沒有異步任務到了須要觸發的時機。若是有,那就開始執行異步任務。
- 重複的執行主線程的任務和輪詢任務隊列。
這種主線程不斷地從任務隊列中讀取任務的機制稱爲 Event Loop
(事件循環)。promise
在講 Event Loop
以前,咱們先來了解一下 macrotask
(宏任務)和 microtask
(微任務)。瀏覽器
包括 setTimeout
、setInterval
、setImmediate
(瀏覽器僅 IE10 支持)、I/O
、UI Rendering
。bash
包括 process.nextTick
(node
獨有)、Promise
、Object.observe
(已廢棄)、MutatinObserver
。異步
這裏多說一句,Promise
的執行函數(也就是 new Promise(fn)
中的 fn
)是同步任務。socket
Event Loop
的實如今瀏覽器和 node
中是不同的,咱們先看瀏覽器。async
- 開始執行主線程的任務
- 主線程的任務執行完畢以後去檢查
microtask
隊列,將已經到了觸發時機的任務放進主線程。- 主線程開始執行任務
- 主線程的任務執行完畢以後去檢查
macrotask
隊列,將已經到了觸發時機的任務放進主線程。- 主線程開始執行任務
- 輪詢
microtask
和microtask
好,講完了流程,來看下🌰。
console.log('script start'); // 同步任務
setTimeout(function() {
console.log('setTimeout'); // 放入 宏任務 隊列
}, 0);
new Promise((resolve, reject) => {
console.log('promise'); // 同步任務
resolve();
})
.then(function() {
console.log('promise1'); // 放進 微任務 隊列
})
.then(function() {
console.log('promise2'); // 放進 微任務 隊列
});
console.log('script end'); // 同步任務
複製代碼
根據上面的標識,先執行同步任務,打印出 「script start」 、 「promise」 、 「script end」,而後開始檢查 microtask
隊列,打印出 「promise1」 和 「promise2」,而後去檢查 macrotask
隊列,打印出 「setTimeout」。
這裏 setTimeout
雖然它的延遲時間爲 0,但它是個宏任務,因此必須等同步任務和微任務執行完畢以後才輪到它。
在看一個🌰。
console.log('script start'); // 同步任務
async function async1() {
await async2();
console.log('async1 end'); // 這裏就是 then 裏面的代碼,放入 微任務 隊列
}
async function async2() {
console.log('async2 end'); // 同步任務
}
async1();
setTimeout(function() {
console.log('setTimeout'); // 放入 宏任務 隊列
}, 0);
new Promise((resolve) => {
console.log('Promise'); // 同步任務
setTimeout(() => {
console.log('setTimeout promise'); // 放入 宏任務 隊列
resolve();
});
})
.then(function() {
console.log('promise1'); // 放入 微任務 隊列
})
.then(function() {
console.log('promise2'); // 放入 微任務 隊列
});
console.log('script end'); // 同步任務
複製代碼
這裏的 async
和 await
就是 Promise
的語法糖,要懂得轉換,其實上述 async
和 await
代碼等價於:
new Promise((resolve) => {
new Promise((resolve) => {
console.log('async2 end');
resolve();
});
resolve();
}).then(() => {
console.log('async1 end');
});
複製代碼
因此執行順序爲
script start
async2 end
promise
script end
async1 end
setTimeout
setTimeout promise
promise1
promise2
複製代碼
有人可能會有疑惑了,打印 「promise1」 和 「promise2」 是微任務,怎麼還晚於 setTimeout
宏任務呢?
雖然它們是微任務,可是因爲觸發它們的 resolve()
處於 setTimeout
宏任務之中,因此它們實際上是在第二輪微任務的輪詢中被觸發的。
好了,瀏覽器的 Event Loop
就說到這個,接下來說一下 node
的 Event Loop
。
node
中的 Event Loop
就比較複雜了,英語好的能夠去看官方文檔。
引用官文檔中的一張圖,瞭解一下 Event Loop
的六個階段。
每一個階段都有本身的任務隊列。
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
複製代碼
timer:執行setTimeout
和setInterval
中到期的 callback。
pending callback:上一輪循環中少數的 callback 會放在這一階段執行。
idle, prepare:僅在內部使用。
poll:最重要的階段,執行 pending callback,在適當的狀況下回阻塞在這個階段。
check:執行setImmediate
(setImmediate()
是將事件插入到事件隊列尾部,主線程和事件隊列的函數執行完成以後當即執行setImmediate
指定的回調函數)的 callback。
close callbacks:執行 close 事件的 callback,例如socket.on('close'[,fn])
或者http.server.on('close, fn)
。
咱們重點關注 timer
、poll
、check
這三個階段。
這個階段執行該階段任務隊列中 setTimeout
和 setInterval
到期的回調,這二者須要設置一個時間。按規則來講,是到了設定的時間以後就應該執行回調,但在實際狀況中,回調函數並非一到設定的時間就能獲得執行的,有可能被其餘的任務阻塞了,須要等其餘任務執行完成以後回調才能獲得執行。
好比你設定一個 100ms 以後的 setTimeout
A 回調,可是在 95ms 時執行了一個其餘的 B 任務,須要耗時 10ms,那麼在時間來到 100ms 的時候,B 任務還在執行當中,那麼此時並不會當即執行 A 回調,而是會再等 5ms,等 B 回調完成以後,而後系統發現 A 回調的觸發時機已經到了,那趕忙去執行 A 回調。也就是說在這種狀況下,A 回調會在 105ms 的時間被執行。
poll 階段主要有兩個事情要作:
I/O
回調。當事件循環到達 poll
階段時,會有下面兩種狀況:
poll
隊列不爲空,那就開始執行隊列中的任務,直到隊列爲空或者達到系統限制。poll
隊列爲空,那麼這種狀況又分兩種狀況;
check
階段有 setImmediate
任務須要執行,那麼就當即結束當前階段,轉到 check
階段執行該階段隊列中的回調。check
階段沒有 setImmediate
任務須要執行,那麼此時會停留在 poll
階段進行等待,等待有任務進到任務隊列中進行執行。在 2.2 的狀況中,還會去檢查 timer
階段有沒有任務到了執行時間,若是有,那麼轉入 timer
階段執行隊列中到期的任務。
此階段會執行 setImmediate
回調,一旦此階段的任務隊列中有了 setImmediate
回調任務,且 poll
階段的任務執行完了,處於空閒狀態,那麼就會當即轉到 check
階段執行此階段任務隊列中的任務。
轉入此階段的條件:check
任務隊列中有了任務,poll
階段處於閒置狀態,或者 poll
階段等待超時。
這二者很類似,也有些不一樣。
setImmediate
設計用於在當前poll
階段完成後 check
階段執行腳本 。setTimeout
安排在通過最小設定時間後運行的腳本,在timers
階段執行。大部分時間 setImmediate
會比 setTimeout
先執行,但也有例外。好比下列代碼:
setImmediate(() => {
console.log('setImmediate');
});
setTimeout(() => {
console.log('setTimeout');
});
複製代碼
若是這兩個任務是在 check
以後 timer
以前加入到各自階段的任務隊列中的,那麼會先執行 setTimeout
,其餘狀況會先執行 setImmediate
。
總的來講,setImmediate
在大部分的狀況下會比 setTimeout
先執行。
從技術上來講,process.nextTick
並不屬於 Event Loop
的一部分,它會在每一個階段執行完畢轉入下一個階段的以前執行。若是有多個 process.nextTick
語句(無論它們是否嵌套),都會在當前階段結束以後所有執行。
好比:
process.nextTick(function A() {
console.log(1);
process.nextTick(function B() {
console.log(2);
});
});
setTimeout(() => {
console.log('setTimeout');
}, 0);
複製代碼
這段代碼會輸出:「1 => 2 => setTimeout」
再來看下 setImmediate
的
setImmediate(function A() {
console.log(1);
setImmediate(function B() {
console.log(2);
});
});
setTimeout(() => {
console.log('setTimeout');
}, 0);
複製代碼
這段代碼的老是在最後輸出 2,說明 setImmediate
會將它裏面的事件註冊到下一個循環中。
因爲 process.nextTick
裏面的 process.nextTick
也會在當前階段執行,那麼若是 process.nextTick
發生了嵌套,那麼就會產生無限循環,不再會轉入其餘階段。
process.nextTick(function foo() {
process.nextTick(foo);
});
複製代碼
node
中的 promise
和 process.nextTick
都屬於微任務,它也會在每一個階段執行完畢以後調用,可是它的優先級會比 process.nextTick
低。