在說EventLoop以前咱們先看一道題html
setTimeout(() => {
console.log(111);
}, 1000);
while (true) {
console.log(22);
}
複製代碼
console.log(111); 永遠都不會輸出,由於javaScript 是單線程html5
咱們常常說JS是單線程執行的,指的是一個進程裏只有一個主線程,那到底什麼是線程?什麼是進程?java
官方的說法是: 進程是CPU資源分配的最小單位;線程是CPU調度的最小單位。 這兩句話並很差理解,咱們先來看張圖:node
以 Chrome 瀏覽器中爲例,當你打開一個 Tab 頁時,其實就是建立了一個進程,一個進程中能夠有多個線程(下文會詳細介紹),好比渲染線程、JS 引擎線程、HTTP 請求線程等等。當你發起一個請求時,其實就是建立了一個線程,當請求結束後,該線程可能就會被銷燬ios
主程序只有一個線程,即同一時間片斷內其只能執行單個任務。ajax
JavaScript的主要用途是與用戶交互,以及操做DOM,若是一個線程是執行刪除操做,一個是修改操做,那麼就會出現問題。所以決定了它只能是單線程,不然會帶來不少複雜的同步問題。axios
單線程就意味着,同一時間只能執行一個任務,全部任務都須要排隊,前一個任務結束,纔會執行後一個任務。若是前一個任務耗時很長,後一個任務就須要一直等着。這就會致使IO操做(耗時但CPU閒置)時形成性能浪費的問題。promise
答案是異步 ,主線程徹底能夠無論IO操做,暫時掛起處於等待中的任務,先運行排在後面的任務。等到IO操做返回告終果,在回過頭,把掛起的任務繼續執行下去。因而,因此任務能夠分紅兩種,一種是同步任務(synchronous),另外一種是異步任務(asynchronous)。瀏覽器
簡單來講瀏覽器內核是經過取得頁面內容,整理信息(應用CSS),計算和組合最終輸出可視化的圖像結果,一般也被成爲渲染引擎。markdown
瀏覽器內核是多線程,在內核控制下各線程相互配合以保持同步,一個瀏覽器一般由一下常駐線程組成:
事件循環中的異步隊列有兩種:macro(宏任務)隊列和micro(微任務)隊列。宏任務隊列能夠有多個,微任務隊列只有一個
一個完整的Event Loop過程,能夠歸納爲如下階段:
一開始執行棧空,咱們能夠把 執行棧認爲是一個存儲函數調用的棧結構,遵循先進後出的原則。micro隊列空,macro隊列裏有且只有script腳本(總體代碼)。
全局上下文(script標籤)被推入執行棧,同步代碼執行。在執行的過程當中,會判斷是同步任務仍是異步任務,經過對一些接口的調用,能夠產生新的macro-task與micro-task,它們會分別被推入各自的任務隊列裏。同步代碼執行完了,script腳本會被移出macro隊列,這個過程本質上是隊列的macro-task的執行和出隊的過程。
上一步咱們出隊的是一個macro-task,這一步咱們處理的是micro-task。但須要注意的是:當macro-task出隊時,任務是一個一個執行的;而micro-task出隊時,任務時一隊一隊執行的。所以,咱們處理micro隊列這一步,會逐個執行隊列中的任務並把它出隊,知道隊列被清空。
執行渲染操做,更新界面
檢查是否存在Web worker任務,若是有,則對其進行處理
上述過程循環往復,知道兩個隊列都清空
咱們總結一下,每次循環都是一個這樣的過程:
當某個宏任務執行完後,會查看是否有微任務隊列。若是有,先執行微任務隊列中的全部任務,若是沒有,會讀取宏任務隊列中排在最前的任務,執行宏任務的過程當中,遇到微任務,依次加入微任務隊列。棧空後,再次讀取微任務隊列裏的任務,依次類推。
接下來咱們看道例子來介紹上面流程:
Promise.resolve().then(()=>{
console.log('Promise1')
setTimeout(()=>{
console.log('setTimeout2')
},0)
})
setTimeout(()=>{
console.log('setTimeout1')
Promise.resolve().then(()=>{
console.log('Promise2')
})
},0)
複製代碼
最後輸出結果是 Promise1,setTimeout1,Promise2,setTimeout2
一開始執行棧的同步任務(這屬於宏任務)執行完畢,會去查看是否有微任務隊列,上題中存在(有且只有一個),而後執行微任務隊列中的全部任務輸出 Promise1,同時會生成一個宏任務 setTimeout2
而後去查看宏任務隊列,宏任務 setTimeout1 在 setTimeout2 以前,先執行宏任務 setTimeout1,輸出 setTimeout1
在執行宏任務 setTimeout1 時會生成微任務 Promise2 ,放入微任務隊列中,接着先去清空微任務隊列中的全部任務,輸出 Promise2
清空完微任務隊列中的全部任務後,就又會去宏任務隊列取一個,這回執行的是 setTimeout2
Node中的Event Loop和瀏覽器中的是徹底不相同的東西。Node.js採用V8做爲js的解析引擎,而I/O處理方面使用了本身設計的libuv,libuv是一個基於事件驅動的跨平臺抽象層,封裝了不一樣操做系統一些底層特性,對外提供統一的API,事件循環機制也是它裏面的實現:
Node.js的運行機制以下:
其中 libuv 引擎中的事件循環分爲 6 個階段,它們會按照順序反覆運行。每當進入某一個階段的時候,都會從對應的回調隊列中取出函數去執行。當隊列爲空或者執行的回調函數數量到達系統設定的閾值,就會進入下一階段。
從上圖中,大體看出 node 中的事件循環的順序:
外部輸入數據-->輪詢階段(poll)-->檢查階段(check)-->關閉事件回調階段(close callback)-->定時器檢測階段(timer)-->I/O 事件回調階段(I/O callbacks)-->閒置階段(idle, prepare)-->輪詢階段(按照該順序反覆運行)...
注意:上面六個階段都不包括 process.nextTick()(下文會介紹)
接下去咱們詳細介紹timers、poll、check這 3 個階段,由於平常開發中的絕大部分異步任務都是在這 3 個階段處理的。
timers 階段會執行 setTimeout 和 setInterval 回調,而且是由 poll 階段控制的。
一樣,在 Node 中定時器指定的時間也不是準確時間,只能是儘快執行。
poll 是一個相當重要的階段,這一階段中,系統會作兩件事情
而且在進入該階段時若是沒有設定了 timer 的話,會發生如下兩件事情
固然設定了 timer 的話且 poll 隊列爲空,則會判斷是否有 timer 超時,若是有的話會回到 timer 階段執行回調。
setImmediate()的回調會被加入 check 隊列中,從 event loop 的階段圖能夠知道,check 階段的執行順序在 poll 階段以後。
咱們先來看個例子:
console.log('start')
setTimeout(() => {
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(() => {
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
Promise.resolve().then(function() {
console.log('promise3')
})
console.log('end')
//start=>end=>promise3=>timer1=>timer2=>promise1=>promise2
複製代碼
一開始執行棧的同步任務(這屬於宏任務)執行完畢後(依次打印出 start end,並將 2 個 timer 依次放入 timer 隊列),會先去執行微任務(這點跟瀏覽器端的同樣),因此打印出 promise3
而後進入 timers 階段,執行 timer1 的回調函數,打印 timer1,並將 promise.then 回調放入 microtask 隊列,一樣的步驟執行 timer2,打印 timer2;這點跟瀏覽器端相差比較大,timers 階段有幾個 setTimeout/setInterval 都會依次執行,並不像瀏覽器端,每執行一個宏任務後就去執行一個微任務(關於 Node 與瀏覽器的 Event Loop 差別,下文還會詳細介紹)。
兩者很是類似,區別主要在於調用時機不一樣。
setTimeout(function timeout () {
console.log('timeout');
},0);
setImmediate(function immediate () {
console.log('immediate');
});
複製代碼
對於以上代碼來講,setTimeout 可能執行在前,也可能執行在後。
首先 setTimeout(fn, 0) === setTimeout(fn, 1),這是由源碼決定的
進入事件循環也是須要成本的,若是在準備時候花費了大於 1ms 的時間,那麼在 timer 階段就會直接執行 setTimeout 回調
若是準備時間花費小於 1ms,那麼就是 setImmediate 回調先執行了
但當兩者在異步 i/o callback 內部調用時,老是先執行 setImmediate,再執行 setTimeout
const fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0)
setImmediate(() => {
console.log('immediate')
})
})
// immediate
// timeout
複製代碼
在上述代碼中,setImmediate 永遠先執行。由於兩個代碼寫在 IO 回調中,IO 回調是在 poll 階段執行,當回調執行完畢後隊列爲空,發現存在 setImmediate 回調,因此就直接跳轉到 check 階段去執行回調了。
這個函數實際上是獨立於 Event Loop 以外的,它有一個本身的隊列,當每一個階段完成後,若是存在 nextTick 隊列,就會清空隊列中的全部回調函數,而且優先於其餘 microtask 執行。
setTimeout(() => {
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
process.nextTick(() => {
console.log('nextTick')
process.nextTick(() => {
console.log('nextTick')
process.nextTick(() => {
console.log('nextTick')
process.nextTick(() => {
console.log('nextTick')
})
})
})
})
// nextTick=>nextTick=>nextTick=>nextTick=>timer1=>promise1
複製代碼
瀏覽器環境下,microtask 的任務隊列是每一個 macrotask 執行完以後執行。而在 Node.js 中,microtask 會在事件循環的各個階段之間執行,也就是一個階段執行完畢,就會去執行 microtask 隊列的任務。
接下咱們經過一個例子來講明二者區別:
setTimeout(()=>{
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(()=>{
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
複製代碼
瀏覽器端運行結果:timer1=>promise1=>timer2=>promise2
瀏覽器端的處理過程以下:
Node 端運行結果:timer1=>timer2=>promise1=>promise2
全局腳本(main())執行,將 2 個 timer 依次放入 timer 隊列,main()執行完畢,調用棧空閒,任務隊列開始執行;
首先進入 timers 階段,執行 timer1 的回調函數,打印 timer1,並將 promise1.then 回調放入 microtask 隊列,一樣的步驟執行 timer2,打印 timer2;
至此,timer 階段執行結束,event loop 進入下一個階段以前,執行 microtask 隊列的全部任務,依次打印 promise一、promise2
Node 端的處理過程以下:
瀏覽器和 Node 環境下,microtask 任務隊列的執行時機不一樣
setTimeout(()=>{
console.log(1)
},0)
let a=new Promise((resolve)=>{
console.log(2)
resolve()
}).then(()=>{
console.log(3)
}).then(()=>{
console.log(4)
})
console.log(5)
複製代碼
以此輸出 2,5,3,4,1
new Promise((resolve,reject)=>{
console.log("promise1")
resolve()
}).then(()=>{
console.log("then11")
new Promise((resolve,reject)=>{
console.log("promise2")
resolve()
}).then(()=>{
console.log("then21")
}).then(()=>{
console.log("then23")
})
}).then(()=>{
console.log("then12")
})
複製代碼
promise1,then11,promise2,then21,then12,then23
new Promise((resolve,reject)=>{
console.log("promise1")
resolve()
}).then(()=>{
console.log("then11")
return new Promise((resolve,reject)=>{
console.log("promise2")
resolve()
}).then(()=>{
console.log("then21")
}).then(()=>{
console.log("then23")
})
}).then(()=>{
console.log("then12")
})
複製代碼
Promise的第二個then至關因而掛在新Promise的最後一個then的返回值上。
promise1,then11,promise2,then21,then23,then12
new Promise((resolve,reject)=>{
console.log("promise1")
resolve()
}).then(()=>{
console.log("then11")
new Promise((resolve,reject)=>{
console.log("promise2")
resolve()
}).then(()=>{
console.log("then21")
}).then(()=>{
console.log("then23")
})
}).then(()=>{
console.log("then12")
})
new Promise((resolve,reject)=>{
console.log("promise3")
resolve()
}).then(()=>{
console.log("then31")
})
複製代碼
promise1,promise3,then11,promise2,then31,then21,then12,then23
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log( 'async2');
}
console.log("script start");
setTimeout(function () {
console.log("settimeout");
},0);
async1();
new Promise(function (resolve) {
console.log("promise1");
resolve();
}).then(function () {
console.log("promise2");
});
console.log('script end');
複製代碼
script start,async1 start,async2,promise1,script end,async1 end,promise2,settimeout
async1 能夠當作以下
funcation async1(){
console.log("async1 start");
new Promise((resolve)=>{
console.log( 'async2');
}).then(()=>{
console.log("async1 end");
})
}
複製代碼
async function async1() {
console.log(1)
await async2()
console.log(2)
return await 3
}
async function async2() {
console.log(4)
}
setTimeout(function() {
console.log(5)
}, 0)
async1().then(v => console.log(v))
new Promise(function(resolve) {
console.log(6)
resolve();
console.log(7)
}).then(function() {
console.log(8)
})
console.log(9)
複製代碼
1,4,6,7,9,2,8,3,5
咱們知道Promise自己是一個異步方法,必須得在執行棧執行完了再去取它的值,所以,全部的返回值都得包一層異步setTimeout。那麼問題來了,爲何Promise的resolve被setTimeout包裹後就成了微任務,要知道setTimeout但是宏任務。
在現代瀏覽器裏面,產生微任務有兩種方式。
第一種是使用MutationObserver監控某個DOM節點,而後在經過JavaScript來修改這個節點,或者爲這個節點添加,刪除部分子節點,當DOM節點發生變化時,就會產生DOM變化記錄的微任務。
第二種方式是使用Promise,當調用Promise.resolve()或者Promise.reject()的時候,也會產生微任務。
ECMAScript規範明確指出Promise必須以Promise Job形式加入job queues(也就是microtask)。Job Queue是ES6中新剔除的概念,創建在事件循環隊列之上。