本文咱們將會介紹 JS 實現異步的原理,而且瞭解了在瀏覽器和 Node 中 Event Loop 實際上是不相同的。html
想閱讀更多優質原創文章請猛戳GitHub博客前端
咱們常常說JS 是單線程執行的,指的是一個進程裏只有一個主線程,那到底什麼是線程?什麼是進程?html5
官方的說法是:進程是 CPU資源分配的最小單位;線程是 CPU調度的最小單位。這兩句話並很差理解,咱們先來看張圖: node
以Chrome瀏覽器中爲例,當你打開一個 Tab 頁時,其實就是建立了一個進程,一個進程中能夠有多個線程(下文會詳細介紹),好比渲染線程、JS 引擎線程、HTTP 請求線程等等。當你發起一個請求時,其實就是建立了一個線程,當請求結束後,該線程可能就會被銷燬。ios
簡單來講瀏覽器內核是經過取得頁面內容、整理信息(應用CSS)、計算和組合最終輸出可視化的圖像結果,一般也被稱爲渲染引擎。git
瀏覽器內核是多線程,在內核控制下各線程相互配合以保持同步,一個瀏覽器一般由如下常駐線程組成:github
好比 setTimeout定時器計數結束, ajax等異步請求成功並觸發回調函數,或者用戶觸發點擊事件時,該線程會將整裝待發的事件依次加入到任務隊列的隊尾,等待 JS引擎線程的執行。web
瀏覽器端事件循環中的異步隊列有兩種:macro(宏任務)隊列和 micro(微任務)隊列。宏任務隊列能夠有多個,微任務隊列只有一個。面試
一個完整的 Event Loop 過程,能夠歸納爲如下階段:ajax
一開始執行棧空,咱們能夠把執行棧認爲是一個存儲函數調用的棧結構,遵循先進後出的原則。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
Node 中的 Event Loop 和瀏覽器中的是徹底不相同的東西。Node.js採用V8做爲js的解析引擎,而I/O處理方面使用了本身設計的libuv,libuv是一個基於事件驅動的跨平臺抽象層,封裝了不一樣操做系統一些底層特性,對外提供統一的API,事件循環機制也是它裏面的實現(下文會詳細介紹)。
其中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 是一個相當重要的階段,這一階段中,系統會作兩件事情
1.回到 timer 階段執行回調
2.執行 I/O 回調
而且在進入該階段時若是沒有設定了 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
複製代碼
Node端事件循環中的異步隊列也是這兩種:macro(宏任務)隊列和 micro(微任務)隊列。
兩者很是類似,區別主要在於調用時機不一樣。
setTimeout(function timeout () {
console.log('timeout');
},0);
setImmediate(function immediate () {
console.log('immediate');
});
複製代碼
但當兩者在異步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=>promise1=>timer2=>promise2
timer1=>promise1=>timer2=>promise2
timer1=>timer2=>promise1=>promise2
(下文過程解釋基於這種狀況下)1.全局腳本(main())執行,將2個timer依次放入timer隊列,main()執行完畢,調用棧空閒,任務隊列開始執行;
2.首先進入timers階段,執行timer1的回調函數,打印timer1,並將promise1.then回調放入microtask隊列,一樣的步驟執行timer2,打印timer2;
3.至此,timer階段執行結束,event loop進入下一個階段以前,執行microtask隊列的全部任務,依次打印promise一、promise2
Node端的處理過程以下:
瀏覽器和Node 環境下,microtask 任務隊列的執行時機不一樣
文章於2019.1.16晚,對最後一個例子在node運行結果,從新修改!再次特別感謝zy445566的精彩點評,因爲node版本更新到11,Event Loop運行原理髮生了變化,一旦執行一個階段裏的一個宏任務(setTimeout,setInterval和setImmediate)就馬上執行微任務隊列,這點就跟瀏覽器端一致。