衆所周知,在使用javascript時,常常須要考慮程序中存在異步的狀況,若是對異步考慮不周,很容易在開發中出現技術錯誤和業務錯誤。做爲一名合格的javascript使用者,瞭解異步的存在和運行機制十分重要且有必要;那麼,異步到底是何方神聖呢?咱們不得不提Event Loop:也叫作事件循環,是指瀏覽器或Node環境的一種解決javaScript單線程運行時不會阻塞的一種機制,也就是實現異步的原理。做爲一種單線程語言,javascript自己是沒有異步這一說法的,是由其宿主環境提供的
(EventLoop優秀文章網上有不少,這篇文章是本身的整合和理解)。
注意:Event Loop 並非在 ECMAScript 標準中定義的,而是在 HTML 標準中定義的;javascript
javascript
代碼運行時,任務被分爲兩種,宏任務(MacroTask/Task)
和微任務(MircoTask)
;Event Loop
在執行和協調各類任務時也將任務隊列分爲Task Queue
和MircoTak Queue
分別對應管理宏任務(MacroTask/Task)
和微任務(MircoTask)
;做爲隊列,Task Queue
和MircoTak Queue
也具有隊列特性:先進先出(FIFO—first in first out)
。java
在 HTML 標準中,並無明確規定 Microtask,可是實際開發中包含如下四種:node
then、catch、finally
(原理參考:【js進階】手撕Promise,一碼一解析 包懂) 基本上,咱們將javascript中非微任務(MircoTask)
的全部任務都歸爲宏任務,好比:編程
javascript runtime:爲 JavaScript 提供一些對象或機制,使它可以與外界交互,是javascript的執行環境。
javascript執行時會建立一個main thread主線程
和call-stack 調用棧(執行棧,遵循後進先出的規則)
,全部的任務都會被放到調用棧/執行棧等待主線程執行
。其運行機制以下:promise
Event Table
,當異步任務有結果後,將相對應的回調函數進行註冊,放入Event Queue
;Event Queue(FIFO)
中讀取任務,放入主線程執行;Event Queue
任務繼續從第一步開始,如此循環執行;上述步驟執行過程就是咱們所說的事件循環(Event Loop),上圖展現了事件循環中的一個完整循環過程。瀏覽器
不一樣的執行環境中,Event Loop的執行機制是不一樣的;例如Chrome 和 Node.js 都使用了 V8 Engine:V8 實現並提供了 ECMAScript 標準中的全部數據類型、操做符、對象和方法(注意並無 DOM)。但它們的 Runtime 並不同:Chrome 提供了 window、DOM,而 Node.js 則是 require、process 等等
。咱們在瞭解瀏覽器中Event Loop的具體表現前須要先整理同步、異步、微任務、宏任務之間的關係!網絡
看到這裏,可能會有不少疑惑:同步異步很好理解,宏任務微任務上面也進行了分類,可是當他們四個在一塊兒後就感受很混亂了,冥冥之中以爲同步異步和宏任務微任務有內在聯繫,可是他們之間有聯繫嗎?又是什麼聯繫呢?網上有的文章說宏任務就是同步的,微任務就是異步的 這種說法明顯是錯的!
其實我更願意如此描述:宏任務和微任務是相對而言的,根據代碼執時循環的前後,將代碼執行分層理解,在每一層(一次)的事件循環中,首先總體代碼塊看做一個宏任務,宏任務中的 Promise(then、catch、finally)、MutationObserver、Process.nextTick就是該宏任務層的微任務;宏任務中的同步代碼進入主線程中當即執行的,宏任務中的非微任務異步執行代碼將做爲下一次循環的宏任務時進入調用棧等待執行的;此時,調用棧中等待執行的隊列分爲兩種,優先級較高先執行的本層循環微任務隊列(MicroTask Queue),和優先級低的下層循環執行的宏任務隊列(MacroTask Queue)!
注意:每一次/層循環,都是首先從宏任務開始,微任務結束;
異步
上面的描敘相對拗口,結合代碼和圖片分析理解:socket
答案暫時不給出,咱們先進行代碼分析:這是一個簡單而典型的雙層循環
的事件循環
執行案例,在這個循環中能夠按照如下步驟進行分析:ide
宏任務
的範圍(整個代碼);宏任務
中同步代碼
和異步代碼
同步代碼:console.log('script start');
、console.log('enter promise');
和console.log('script end');
;
異步代碼塊:setTimeout
和Promise的then
(注意:Promise中只有then、catch、finally的執行須要等到結果,Promise傳入的回調函數屬於同步執行代碼
);
異步
中找出同層的微任務
(代碼中的Promise的then
)和下層事件循環的宏任務
(代碼中的setTimeout
)宏任務
的同步代碼優先進入主線程
,按照自上而下順序執行完畢;輸出順序爲:
//同步代碼執行輸出 script start enter promise script end
微任務
//同層微任務隊列代碼執行輸出 promise then 1 promise then 2
setTimeout
包含的執行代碼,只有一個同步代碼)//第二層宏任務隊列代碼執行輸出 setTimeout
綜合分析最終得出數據結果爲:
//首層宏任務代碼執行輸出 script start enter promise script end //首層微任務隊列代碼執行輸出 promise then 1 promise then 2 //第二層宏任務隊列代碼執行輸出 setTimeout
那麼,你是否已經瞭解上述執行過程了呢?若是徹底理解上述實例,說明你已經大概知道瀏覽器中Event Loop的執行機制,可是,要想知道本身是否是徹底明白,不妨對於下列多循環的事件循環進行分析檢驗,給出你的結果:
console.log('1'); setTimeout(function() { console.log('2'); new Promise(function(resolve) { console.log('3'); resolve(); }).then(function() { console.log('4') }) setTimeout(function() { console.log('5'); new Promise(function(resolve) { console.log('6'); resolve(); }).then(function() { console.log('7') }) }) console.log('14'); }) new Promise(function(resolve) { console.log('8'); resolve(); }).then(function() { console.log('9') }) setTimeout(function() { console.log('10'); new Promise(function(resolve) { console.log('11'); resolve(); }).then(function() { console.log('12') }) }) console.log('13')
分析:以下圖草稿所示,左上角標a爲宏任務隊列,左上角標i爲微任務隊列
,同一層循環中,本層宏任務先執行,再執行微任務;本層宏任務中的非微任務異步代碼塊做爲下層循環的宏任務進入下次循環,如此循環執行;
若是你的與下面的結果一致,恭喜你瀏覽器環境的Event Loop
你已經徹底掌握,那麼請開始下面的學習:
1->8->13->9->2->3->14->4->10->11->12->5->6->7
在Node
環境下,瀏覽器的EventLoop
機制並不適用,切記不能混爲一談。這裏借用網上不少博客上的一句總結(其實我也是真不太懂):Node
中的Event Loop
是基於libuv實現的:libuv
是 Node
的新跨平臺抽象層,libuv
使用異步,事件驅動的編程方式,核心是提供i/o
的事件循環和異步回調。libuv
的API
包含有時間,非阻塞的網絡,異步文件操做,子進程等等。
Node的Event loop一共分爲6個階段
,每一個細節具體以下:
timers:
執行setTimeout和setInterval中到期的callback。pending callback:
上一輪循環中少數的callback會放在這一階段執行。idle, prepare:
僅在內部使用。poll:
最重要的階段,執行pending callback,在適當的狀況下回阻塞在這個階段。check:
執行setImmediate的callback。close callbacks:
執行close事件的callback,例如socket.on('close'[,fn])或者http.server.on('close, fn)。注意:上面六個階段都不包括 process.nextTick()
重點:如上圖所,在Node.js中,一次宏任務能夠認爲是包含上述6個階段、微任務microtask會在事件循環的各個階段之間執行,也就是一個階段執行完畢,就會去執行microtask隊列的任務。
在第二節中就瞭解到,process.nextTick()
屬於微任務,可是這裏須要重點說起下:
process.nextTick()
雖然它是異步API的一部分,但未在圖中顯示。由於process.nextTick()
從技術上講,它不是事件循環的一部分;能夠理解爲微任務中優先級最高的
)老規矩,線上代碼:
console.log('1'); setTimeout(function() { console.log('2'); process.nextTick(function() { console.log('3'); }) new Promise(function(resolve) { console.log('4'); resolve(); }).then(function() { console.log('5') }) }) process.nextTick(function() { console.log('6'); }) new Promise(function(resolve) { console.log('7'); resolve(); }).then(function() { console.log('8') }) setTimeout(function() { console.log('9'); process.nextTick(function() { console.log('10'); }) new Promise(function(resolve) { console.log('11'); resolve(); }).then(function() { console.log('12') }) }) console.log('13')
將代碼的執行分區進行解釋
分析:以下圖草稿所示,左上角標a爲宏任務隊列,左上角標i爲微任務隊列
,左上角標t爲timers階段隊列
,左上角標p爲nextTick隊列
同一層循環中,本層宏任務先執行,再執行微任務;本層宏任務中的非微任務異步代碼塊做爲下層循環的宏任務進入下次循環,如此循環執行:
總體代碼
能夠看作宏任務,同步代碼直接進入主線程執行,輸出1,7,13
,接着執行同層微任務且nextTick優先執行輸出6,8
;setTimeout
,兩個setTimeout代碼塊依次進入6階段中的timer階段
以t一、t2
進入隊列;代碼等價於:setTimeout(function() { console.log('2'); process.nextTick(function() { console.log('3'); }) new Promise(function(resolve) { console.log('4'); resolve(); }).then(function() { console.log('5') }) }) setTimeout(function() { console.log('9'); process.nextTick(function() { console.log('10'); }) new Promise(function(resolve) { console.log('11'); resolve(); }).then(function() { console.log('12') }) })
setTimeout
中的同步代碼當即執行輸出2,4,9,11
,nextTick
和Pormise.then
進入微任務執行輸出3,10,5,12
;6階段中的其餘階段
,循環完畢,最終輸出結果爲:1->7->13->6->8->2->4->9->11->3->10->5->12
;console.log('1'); setTimeout(function() { console.log('2'); process.nextTick(function() { console.log('3'); }) new Promise(function(resolve) { console.log('4'); resolve(); }).then(function() { console.log('5') setTimeout(function() { console.log('6'); process.nextTick(function() { console.log('7'); }) new Promise(function(resolve) { console.log('8'); resolve(); }).then(function() { console.log('9') }) }) }) }) process.nextTick(function() { console.log('10'); }) new Promise(function(resolve) { console.log('11'); resolve(); }).then(function() { console.log('12') setTimeout(function() { console.log('13'); process.nextTick(function() { console.log('14'); }) new Promise(function(resolve) { console.log('15'); resolve(); }).then(function() { console.log('16') }) }) }) setTimeout(function() { console.log('17'); process.nextTick(function() { console.log('18'); }) new Promise(function(resolve) { console.log('19'); resolve(); }).then(function() { console.log('20') }) }) console.log('21')
瀏覽器
和 Node
環境下,microtask 任務隊列
的執行時機不一樣:Node 端,microtask 在事件循環的各個階段之間執行;瀏覽器端,microtask 在事件循環的 macrotask 執行完以後執行;