從一道面試題談?wù)剬?EventLoop 的理解
作者:前端自學(xué)驛站,原文鏈接:https://juejin.im/post/6868849475008331783
前言
因為掘金改版之后對于字數(shù)有了一定的限制(親測了下在12500字左右,所以看到標(biāo)題還有幾萬字長文的標(biāo)題一定是在唬你的?)文章美化排版之后字數(shù)超出了限制所以打算將后面的部分單獨拎出來寫, 這樣也更好的寫出相對比較深入的一點的內(nèi)容, 對于【前端體系】這類文章內(nèi)容一定是包括但不限于標(biāo)題的,我會盡可能的拓展、深入、以寫出高質(zhì)量的好文章。
在線卑微,如果覺得這篇文章對你有幫助的話歡迎大家點個贊?
從一道題引出對Event Loop的思考
對于Event Loop(事件輪詢)所涉及的知識概念太多了,如果上來就講一大堆概念性的東西太枯燥且從一開始就是按照我的思路來走的,所以我打算換一種方式來寫這篇文章,你先按照你之前對于Event Loop(事件輪詢)的理解來解這道題,我在后面寫出我從Event Loop的理解思考這題的方式。兩種不同的理解、想法、互相碰撞,我可能有理解不對的,你也可能有之前忽略的一些知識點,我們

不好意思放錯了?,這張圖

題目
console.log('script?start');
setTimeout(()?=>?{
??console.log('北歌');
},?1?*?2000);
Promise.resolve()
.then(function()?{
??console.log('promise1');
}).then(function()?{
??console.log('promise2');
});
async?function?foo()?{
??await?bar()
??console.log('async1?end')
}
foo()
async?function?errorFunc?()?{
??try?{
????await?Promise.reject('error!!!')
??}?catch(e)?{
????console.log(e)
??}
??console.log('async1');
??return?Promise.resolve('async1?success')
}
errorFunc().then(res?=>?console.log(res))
function?bar()?{
??console.log('async2?end')?
}
console.log('script?end');
好了,可以暫時不往下翻,先按照自己的理解來解下這道題。
-------------------------我是分割線-------------------------
相信這道題肯定難不倒大家,但是大家是按照什么樣的方式來解出這道題的呢?其實這道題考察你了很多知識點,下面我將用我的理解來說說對這道題的思考。水平有限、有任何問題歡迎評論區(qū)指出。
JS的運行機制

先來解釋上圖中出現(xiàn)的幾個單詞所要表達的含義。
Heap(堆)、Stack(棧)、Queue(隊列)、Event Loop(事件輪詢)
程序中的堆棧隊列
Heap(堆)
堆, 是一種動態(tài)存儲結(jié)構(gòu),是利用完全二叉樹維護的一組數(shù)據(jù),堆分為兩種,一種為最大堆,一種為最小堆,將根節(jié)點最大的堆叫做最大堆或大根堆,根節(jié)點最小的堆叫做最小堆或小根堆。堆是線性數(shù)據(jù)結(jié)構(gòu),相當(dāng)于一維數(shù)組,有唯一后繼。

棧(Stack)
棧在程序中的設(shè)定是限定僅在表尾進行插入或刪除操作的線性表。棧是一種數(shù)據(jù)結(jié)構(gòu),它按照后進先出(LIFO: last-in-first-out)的原則存儲數(shù)據(jù),先進入的數(shù)據(jù)被壓入棧底,最后的數(shù)據(jù)在棧頂,需要讀數(shù)據(jù)的時候從棧頂開始彈出數(shù)據(jù)。棧是只能在某一端插入和刪除的特殊線性表。

隊列(Queue)
隊列特殊之處在于它只允許在表的前端(front)進行刪除操作,而在表的后端(rear)進行插入操作,和棧一樣,隊列是一種操作受限制的線性表。進行插入操作的端稱為隊尾,進行刪除操作的端稱為隊頭。?隊列中沒有元素時,稱為空隊列。
隊列的數(shù)據(jù)元素又稱為隊列元素。在隊列中插入一個隊列元素稱為入隊,從隊列中刪除一個隊列元素稱為出隊。因為隊列只允許在一端插入,在另一端刪除,所以只有最早進入隊列的元素才能最先從隊列中刪除,故隊列又稱為先進先出(FIFO: first-in-first-out)

js中的堆棧隊列
下面我解釋下JavaScript語言中的堆、棧、隊列。
堆
堆, 動態(tài)分配的內(nèi)存,大小不定也不會自動釋放,存放引用類型,指那些可能由多個值構(gòu)成的對象,保存在堆內(nèi)存中,包含引用類型的變量,實際上保存的不是變量本身,而是指向該對象的指針。可以簡單理解為存儲代碼塊。
堆的作用:存儲引用類型值的數(shù)據(jù)
let?obj?=?{
????name:?'北歌',
????puslic:?'前端自學(xué)驛站'
}
let?func?=?()?=>?{
????console.log('hello?world')
}

棧
js中的棧準確來將應(yīng)該叫調(diào)用棧(EC Stack),會自動分配內(nèi)存空間,會自動釋放,存放基本類型,簡單的數(shù)據(jù)段,占據(jù)固定大小的空間。
棧的作用:存儲基本類型值,還有一個很要的作用。提供代碼執(zhí)行的環(huán)境
隊列
js中的隊列可以叫做任務(wù)隊列或異步隊列,任務(wù)隊列里存放各種異步操作所注冊的回調(diào),里面分為兩種任務(wù)類型,宏任務(wù)(macroTask)和微任務(wù)(microTask)。
好,下面可以回到正題上來了。
為什么會出現(xiàn)Event Loop
總所周知JS是一門單線程的非阻塞腳本語言,Event Loop就是為了解決JS異步編程的一種解決方案。

JS為什么是單線程語言,那它是怎么實現(xiàn)異步編程(非阻塞)運行的
第一個問題:JavaScript的誕生就是為了處理瀏覽器網(wǎng)頁的交互(DOM操作的處理、UI動畫等), ?設(shè)計成單線程的原因就是不想讓瀏覽器變得太復(fù)雜,因為多線程需要共享資源、且有可能修改彼此的運行結(jié)果(兩個線程修改了同一個DOM節(jié)點就會產(chǎn)生不必要的麻煩),這對于一種網(wǎng)頁腳本語言來說這就太復(fù)雜了。
第二個問題:JavaScript是單線程的但它所運行的宿主環(huán)境—瀏覽器是多線程,瀏覽器提供了各種線程供Event Loop調(diào)度來協(xié)調(diào)JS單線程運行時不會阻塞。
小結(jié)
先總結(jié)一波個人對于JS運行機制的理解:
代碼執(zhí)行開啟一個全局調(diào)用棧(主棧)提供代碼運行的環(huán)境,在執(zhí)行過程中同步任務(wù)的代碼立即執(zhí)行,遇到異步任務(wù)將異步的回調(diào)注冊到任務(wù)隊列中,等待同步代碼執(zhí)行完畢查看異步是否完成,如果完成將當(dāng)前異步任務(wù)的回調(diào)拿到主棧中執(zhí)行
進程和線程
進程:進程是 CPU 資源分配的最小單位(是能擁有資源和獨立運行的最小單位)
線程:線程是 CPU 調(diào)度的最小單位(線程是建立在進程的基礎(chǔ)上的一次程序運行單位)
對于進程和線程并沒有確切統(tǒng)一的描述,可以簡單的理解:
比如一個應(yīng)用程序: 如QQ、瀏覽器啟動時會開啟一個進程,而該進程可以有多個線程來進行資源調(diào)度和分配,達到運行程序的作用。
更通俗的話講:打開QQ應(yīng)用程序開啟了進程來運行程序(QQ), 有多個線程來進行資源調(diào)度和分配(多個線程來分配打開QQ所占用的運存),達到運行程序(QQ)的作用.
用操作系統(tǒng)來作個例子:

線程依賴進程,一個進程可以有一個或者多個線程,但是線程只能是屬于一個進程。
JS的單線程
js的單線程指的是javaScript引擎只有一個線程
單線程就意味著,所有任務(wù)需要排隊,前一個任務(wù)結(jié)束,才會執(zhí)行后一個任務(wù)。如果前一個任務(wù)耗時很長,后一個任務(wù)就不得不一直等著。js 引擎執(zhí)行異步代碼而不用等待,是因有為有任務(wù)隊列和事件輪詢。
- 任務(wù)隊列:任務(wù)隊列是一個先進先出的隊列,它里面存放著各種任務(wù)回調(diào)。
- 事件輪詢:事件輪詢是指主線程重復(fù)從任務(wù)隊列中取任務(wù)、執(zhí)行任務(wù)的過程。
瀏覽器的多線程
GUI 渲染線程
- 繪制頁面,解析 HTML、CSS,構(gòu)建 DOM 樹,布局和繪制等
- 頁面重繪和回流
- 與 JS 引擎線程互斥,也就是所謂的 JS 執(zhí)行阻塞頁面更新
JS 引擎線程
- 負責(zé) JS 腳本代碼的執(zhí)行
- 負責(zé)準執(zhí)行準備好待執(zhí)行的事件,即定時器計數(shù)結(jié)束,或異步請求成功并正確返回的事件
- 與 GUI 渲染線程互斥,執(zhí)行時間過長將阻塞頁面的渲染
事件觸發(fā)線程
- 負責(zé)將準備好的事件交給 JS 引擎線程執(zhí)行
- 多個事件加入任務(wù)隊列的時候需要排隊等待(JS 的單線程)
定時器觸發(fā)線程
- 負責(zé)執(zhí)行異步的定時器類的事件,如 setTimeout、setInterval
- 定時器到時間之后把注冊的回調(diào)加到任務(wù)隊列的隊尾
HTTP 請求線程
- 負責(zé)執(zhí)行異步請求
- 主線程執(zhí)行代碼遇到異步請求的時候會把函數(shù)交給該線程處理,當(dāng)監(jiān)聽到狀態(tài)變更事件,如果有回調(diào)函數(shù),該線程會把回調(diào)函數(shù)加入到任務(wù)隊列的隊尾等待執(zhí)行
Event Loop
呼,終于回到正題了!
對于事件輪詢上面其實已經(jīng)解釋的很清楚了:
事件輪詢就是解決javaScript單線程對于異步操作的一些缺陷,讓 javaScript做到既是單線程,又絕對不會阻塞的核心機制,是用來協(xié)調(diào)各種事件、用戶交互、腳本執(zhí)行、UI 渲染、網(wǎng)絡(luò)請求等的一種機制。
瀏覽器中的Eveent Loop執(zhí)行順序
Processing model[1]規(guī)范定義了Eveent Loop的循環(huán)過程:
一個Eveent Loop只要存在,就會不斷執(zhí)行下邊的步驟:
- 1.在tasks(任務(wù))隊列中選擇最老的一個task,用戶代理可以選擇任何task隊列,如果沒有可選的任務(wù),則跳到下邊的microtasks步驟。
- 2.將上邊選擇的task設(shè)置為正在運行的task[2]。
- 3.Run: 運行被選擇的task。
- 4.將Eveent Loop的currently running task[3]變?yōu)閚ull。
- 5.從task隊列里移除前邊運行的task。
- 6.Microtasks: 執(zhí)行microtasks任務(wù)檢查點[4]。(也就是執(zhí)行microtasks隊列里的任務(wù))
- 7.更新渲染(Update the rendering):可以簡單理解為瀏覽器渲染...
- 8.如果這是一個worker event loop,但是沒有任務(wù)在task隊列中,并且WorkerGlobalScope[5]對象的closing標(biāo)識為true,則銷毀Eveent Loop,中止這些步驟,然后進行定義在Web workers[6]章節(jié)的run a worker[7]。
- 9.返回到第一步。
Eveent Loopp會不斷循環(huán)上面的步驟,概括說來:
Eveent Loop會不斷循環(huán)的去取tasks隊列的中最老的一個task(可以理解為宏任務(wù))推入棧中執(zhí)行,并在當(dāng)次循環(huán)里依次執(zhí)行并清空microtask隊列里的任務(wù)。- 執(zhí)行完
microtask隊列里的任務(wù),有可能會渲染更新。(瀏覽器很聰明,在一幀以內(nèi)的多次dom變動瀏覽器不會立即響應(yīng),而是會積攢變動以最高60HZ(大約16.7ms每幀)的頻率更新視圖)
宏任務(wù)和微任務(wù)優(yōu)先問題
在任務(wù)對列(queue)中注冊的異步回調(diào)又分為兩種類型,宏任務(wù)和微任務(wù)。我們?yōu)榱朔奖憷斫饪梢哉J為在任務(wù)隊列中有宏任務(wù)隊列和微任務(wù)隊列。宏任務(wù)隊列有多個,微任務(wù)只有一個
宏任務(wù)(macro Task)
- script(整體代碼)
- setTimeout/setInterval
- setImmediate(Node環(huán)境)
- UI 渲染
- requestAnimationFrame
- ....
微任務(wù)(micro Task)
Promise的then()、catch()、finally()里面的回調(diào)
process.nextTick(Node 環(huán)境)
...
個人理解的執(zhí)行順序:
代碼從開始執(zhí)行調(diào)用一個全局執(zhí)行棧,script標(biāo)簽作為宏任務(wù)執(zhí)行
執(zhí)行過程中同步代碼立即執(zhí)行,異步代碼放到任務(wù)隊列中,任務(wù)隊列存放有兩種類型的異步任務(wù),宏任務(wù)隊列,微任務(wù)隊列。
同步代碼執(zhí)行完畢也就意味著第一個宏任務(wù)執(zhí)行完畢(script)
1、先查看任務(wù)隊列中的微任務(wù)隊列是否存在宏任務(wù)執(zhí)行過程中所產(chǎn)生的微任務(wù)
1-1、有的話就將微任務(wù)隊列中的所有微任務(wù)清空
2-2、微任務(wù)執(zhí)行過程中所產(chǎn)生的微任務(wù)放到微任務(wù)隊列中,在此次執(zhí)行中一并清空
2、如果沒有再看看宏任務(wù)隊列中有沒有宏任務(wù),有的話執(zhí)行,沒有的話事件輪詢第一波結(jié)束
2-1、執(zhí)行過程中所產(chǎn)生的微任務(wù)放到微任務(wù)隊列
2-2、完成宏任務(wù)之后執(zhí)行清空微任務(wù)隊列的代碼

所以是宏任務(wù)優(yōu)先,在宏任務(wù)執(zhí)行完畢之后才會來一次性清空任務(wù)隊列中的所有微任務(wù)。
解題分析過程
將最開始的那道題搬下來
//?=>?代碼一執(zhí)行就開始執(zhí)行了一個宏任務(wù)-宏0
console.log('script?start');?
setTimeout(()?=>?{?//?宏?1
??console.log('北歌');
},?1?*?2000);
Promise.resolve()
????.then(function()?{?//?微1-1
??????console.log('promise1');
????})
????.then(function()?{?//?微1-4 =>?這個then中的會等待上一個then執(zhí)行完成之后得到其狀態(tài)才會向Queue注冊狀態(tài)對應(yīng)的回調(diào),假設(shè)上一個then中主動拋錯且沒有捕獲,那就注冊的是這個then中的第二個回調(diào)了。
??????console.log('promise2');?
????});
async?function?foo()?{
??await?bar()?//?=>?await(promise的語法糖),會異步等待獲取其返回值
??//?=>?后面的代碼可以理解為放到異步隊列微任務(wù)中。?這里可以保留疑問后面會詳細說
??console.log('async1?end')?//?微1-2
}
foo()
function?bar()?{
??console.log('async2?end')?
}
async?function?errorFunc?()?{
??try?{
????await?Promise.reject('error!!!')
??}?catch(e)?{
??????//?=>?從這后面開始所有的代碼可以理解為放到異步隊列微任務(wù)中
????console.log(e)??//?微1-3
??}
??console.log('async1');
??return?Promise.resolve('async1?success')
}
errorFunc().then(res?=>?console.log(res))?//?微1-5
console.log('script?end');
上面代碼對于Promise的用法我就不多講了, 在后面我會寫關(guān)于Promise源碼解析的文章。
注意一點就是Promise.then().then(),在注冊異步任務(wù)的時候,第二個then中的回調(diào)是依賴第一個then中回調(diào)的結(jié)果的,如果執(zhí)行沒有異常才會在該異步任務(wù)執(zhí)行完畢之后注冊狀態(tài)對應(yīng)的回調(diào)
第一次執(zhí)行
全局一個宏任務(wù)執(zhí)行, 輸出同步代碼。掛載宏1、微1-1、微1-2、微1-3、微1-4。1-表示屬于第一次輪詢
run:?script?start、?async2?end、script?end
第二次執(zhí)行
同步代碼執(zhí)行完畢,開始執(zhí)行異步任務(wù)中的微任務(wù)隊列中的代碼。
微任務(wù)隊列:只有一個隊列且會在當(dāng)前輪詢一次清空
run:
?執(zhí)行微1-1:?promise1
?執(zhí)行微1-2:?async1?end
?執(zhí)行微1-3: error!!!、async1 。當(dāng)前異步回調(diào)執(zhí)行完畢才Promise.resolve('async1?success'),然后注冊then()中的成功的回調(diào)-微1-5
?執(zhí)行微1-4:?promise2
????執(zhí)行剛剛注冊的微1-5:?async1?success
到這第一波輪詢結(jié)束
第三次執(zhí)行
開啟第二波輪詢:執(zhí)行宏1
run:?'北歌'
到這。整個輪詢結(jié)束。
其實相對難以理解的也就是微任務(wù),對于微任務(wù)也就是上面說的只有一個隊列會在此次輪詢中一次清空(包括此次執(zhí)行過程中所產(chǎn)生的微任務(wù))。
舉個栗子?
你去堂食排隊打菜,原本你計劃今天只吃兩個菜(微任務(wù)隊列中只注冊了兩個回調(diào)),在打菜的過程中你看到你最喜歡吃的紅燒肉(微任務(wù)執(zhí)行的過程中遇到的新的微任務(wù)),你肯定得再加個菜(將微任務(wù)加入到微任務(wù)隊列)
打怪進階
通過上面的講解現(xiàn)在可以刷幾道題來看看自己撐握的怎么樣了。
黃金題
console.log('1');
setTimeout(()?=>?{
??console.log('2');
??Promise.resolve().then(()?=>?{
????console.log('3');
??})
??new?Promise((resolve)?=>?{
????console.log('4');
????resolve();
??}).then(()?=>?{
????console.log('5')
??})
})
Promise.reject().then(()?=>?{
??console.log('13');
},?()?=>?{
??console.log('12');
})
new?Promise((resolve)?=>?{
??console.log('7');
??resolve();
}).then(()?=>?{
??console.log('8')
})
setTimeout(()?=>?{
??console.log('9');
??Promise.resolve().then(()?=>?{
????console.log('10');
??})
??new?Promise((resolve)?=>?{
????console.log('11');
????resolve();
??}).then(()?=>?{
????console.log('12')
??})
})
磚石題
new?Promise((resolve,?reject)?=>?{
????console.log(1)
????resolve()
??})
??.then(()?=>?{
????console.log(2)
????new?Promise((resolve,?reject)?=>?{
????????console.log(3)
????????setTimeout(()?=>?{
??????????reject();
????????},?3?*?1000);
????????resolve()
????})
??????.then(()?=>?{
????????console.log(4)
????????new?Promise((resolve,?reject)?=>?{
????????????console.log(5)
????????????resolve();
??????????})
??????????.then(()?=>?{
????????????console.log(7)
??????????})
??????????.then(()?=>?{
????????????console.log(9)
??????????})
??????})
??????.then(()?=>?{
????????console.log(8)
??????})
??})
??.then(()?=>?{
????console.log(6)
??})
王者題
Promise.resolve()
??.then(()?=>?{
????console.log('promise1');
????return?new?Promise((resolve,?reject)?=>?{
????????setTimeout(()?=>?{
??????????console.log('timer2')
??????????resolve()
????????},?0)
????})
??????.then(async?()?=>?{
????????await?foo();
????????return?new?Error('error1')
??????})
??????.then((ret)?=>?{
????????setTimeout(()?=>?{
??????????console.log(ret);
??????????Promise.resolve()
??????????.then(()?=>?{
????????????return?new?Error('error!!!')
??????????})
??????????.then(res?=>?{
????????????console.log("then:?",?res)
??????????})
??????????.catch(err?=>?{
????????????console.log("catch:?",?err)
??????????})
????????},?1?*?3000)
??????},?err?=>?{
????????console.log(err);
??????})
??????.finally((res)?=>?{
????????console.log(res);
????????throw?new?Error('error2')
??????})
??????.then((res)?=>?{
????????console.log(res);
??????},?err?=>?{
????????console.log(err);
??????})
??})
??.then(()?=>?{
????console.log('promise2');
??})
function?foo()?{
??setTimeout(()?=>?{?
????console.log('async1');
??},?2?*?1000);
}
setTimeout(()?=>?{
??console.log('timer1')
??Promise.resolve()
????.then(()?=>?{
??????console.log('promise3')
????})
},?0)
console.log('start');
榮耀王者
下面讓我們來一起做最后這道題。
async?function?async1()?{
??console.log('async1?start');
??new?Promise((resolve,?reject)?=>?{
????try?{
??????throw?new?Error('error1')
????}?catch(e)?{
??????console.log(e);
????}
????setTimeout(()?=>?{?//?宏3
??????resolve('promise4')
????},?3?*?1000);
??})
????.then((res)?=>?{?//?微3-1
??????console.log(res);
????},?err?=>?{
??????console.log(err);
????})
????.finally(res?=>?{?//?微3-2?//?TODO注3
??????console.log(res);
????})
??console.log(await?async2());?//?TODO-注1
??console.log('async1?end');?//?微1-1?//?TODO-注2
}
function?async2()?{
??console.log('async2');
??return?new?Promise((resolve)?=>?{
????setTimeout(()?=>?{?//?宏4
??????resolve(2)
????},?1?*?3000);
??})
}
console.log('script?start');
setTimeout(()?=>?{?//?宏2
??console.log('setTimeout');
},?0)
async1();
new?Promise((resolve)?=>?{
??console.log('promise1');
??resolve();
})
??.then(()?=>?{?//?微1-2
????console.log('promise2');
????return?new?Promise((resolve)?=>?{
??????resolve()
????})
??????.then(()?=>?{?//?微1-3
????????console.log('then?1-1')
??????})
??})
??.then(()?=>?{?//?微1-4
????console.log('promise3');
??})
console.log('script?end');
規(guī)定
現(xiàn)在為了方便大家理解,請記住一下規(guī)定:
- 分析以每次輪詢做為分析,同步代碼塊是直接輸出結(jié)果的
- 異步任務(wù)代碼塊中,紅色表示宏任務(wù),綠色表示微任務(wù)
微1-表示第一次輪詢中的微任務(wù)隊列中的所有微任務(wù),微2-表示第二次,以此類推
第一次輪詢
script標(biāo)簽(宏0)執(zhí)行
輸出同步代碼:
script?start?->?async1?start?->?error1?->?async2?->?promise1?->?script?end
掛載異步任務(wù):
-()?=>?{?//?宏2
-??console.log('setTimeout');
-}
-()?=>?{?//?宏3
-??resolve('promise4')
-}
-()?=>?{?//?宏4
-??resolve(2)
-}
+()?=>?{?//?微1-1
+??console.log('promise2');
+??return?new?Promise((resolve)?=>?{
+??resolve()
+}
同步代碼完畢的同時第一個宏任務(wù)也執(zhí)行完畢,開始清空異步任務(wù)中的微任務(wù)隊列:
微1-1:?promise2?->?微1-2:?then?1-1?->?微1-3:?promise3
執(zhí)行微任務(wù)中所產(chǎn)生新的微任務(wù), 放到微任務(wù)隊列中,該微任務(wù)也會在此次輪詢中被清空:
+()?=>?{?//?微1-2
+??console.log('then?1-1')
+}
+()?=>?{?//?微1-3
+??console.log('promise3');
+}
執(zhí)行微任務(wù)過程中所產(chǎn)生的宏任務(wù)放到新宏任務(wù)隊列中:
本次微任務(wù)執(zhí)行沒有產(chǎn)生新的宏任務(wù)
注1
這里得說一下,很多人認為await將代碼同步化的意思,其實await是Promise的語法糖,內(nèi)部的實現(xiàn)也是依靠Promise, 其誕生也就是為了優(yōu)化promise的then鏈寫法,用同步的方式編寫異步代碼,讓代碼看起來更簡潔明了 await的真實意思是 async wait(異步等待的意思)await表達式相當(dāng)于調(diào)用后面返回promise的then方法,異步(等待)獲取其返回值。即 await<==>promise.then
這里的async2函數(shù)返回的Promise中開啟了一個宏任務(wù),await異步等待需要等待宏任務(wù)執(zhí)行才能獲取其返回值,也就是說宏任務(wù)不執(zhí)行await表達式就壓根不能調(diào)用Promise的then方法
注2
前面說過await表達式后面的代碼可以簡單理解為放入到微任務(wù)中,但是前面await 表達式壓根就沒有獲取異步等待的結(jié)果這后面的代碼從跳出當(dāng)前執(zhí)行棧后就壓根沒有掛載到異步任務(wù)中,有些教程說的await 表達式后面的代碼可以看成微任務(wù)隊列的第一個這種說法是錯誤的!
此次輪詢結(jié)束輸出結(jié)果有:
script?start?->?async1?start?->?error1?->?async2?->?promise1?->?script?end
微1-1:?promise2?->?微1-2:?then?1-1?->?微1-3:?promise3
第二次輪詢
上面第一波輪詢結(jié)束,開啟第二波輪詢
執(zhí)行第二個宏任務(wù)隊列(宏任務(wù)隊列只存放一個宏任務(wù)):
宏2:setTimeout
宏任務(wù)執(zhí)行完畢沒有產(chǎn)生新的微任務(wù),也沒有產(chǎn)生新的宏任務(wù)。第二次輪詢結(jié)束
此次輪詢結(jié)束輸出結(jié)果有:
宏2:setTimeout
第三次輪詢
執(zhí)行第三個宏任務(wù)隊列(宏任務(wù)隊列只存放一個宏任務(wù)):
宏任務(wù)本身沒有輸出啥,不過確定了下Promise的狀態(tài)并傳遞了個'promise4'給下一個then中的成功回調(diào)
宏任務(wù)執(zhí)行過程中產(chǎn)生的新的微任務(wù)放到微任務(wù)隊列:
+(res)?=>?{?//?微3-1
+??console.log(res);
+}
宏任務(wù)執(zhí)行完畢開始清空異步任務(wù)中的微任務(wù)隊列:
微3-1:?promise4?->?微3-2:?undefined
執(zhí)行微任務(wù)中所產(chǎn)生新的微任務(wù), 放到微任務(wù)隊列中,該微任務(wù)也會在此次輪詢中被清空:
+res?=>?{?//?微3-2?//?TODO注3
+??console.log(res);
+}
執(zhí)行微任務(wù)過程中所產(chǎn)生的宏任務(wù)放到新宏任務(wù)隊列中:
本次微任務(wù)執(zhí)行沒有產(chǎn)生新的宏任務(wù)
此次輪詢結(jié)束輸出結(jié)果有:
微3-1:?promise4?->?微3-2:?undefined
注3
前面說過promise.finally()也是微任務(wù),finally可以理解為不管promise的狀態(tài)是成功或失敗都要執(zhí)行我。但是我不接受任何結(jié)果。因此finally接受不到返回值res為undefined
第四次輪詢
執(zhí)行第四個宏任務(wù)隊列(宏任務(wù)隊列只存放一個宏任務(wù)):
宏任務(wù)本身沒有輸出啥,不過確定了下Promise的狀態(tài)并傳遞了個2給下一個then中的成功回調(diào)
宏任務(wù)執(zhí)行過程中產(chǎn)生的新的微任務(wù)放到微任務(wù)隊列:
上面說過await => Promise.then(), 上面宏任務(wù)執(zhí)行完畢確定了promise狀態(tài)可以去獲取異步等待的結(jié)果。相當(dāng)于這樣:Promise.then((res) => {return res})而await表達式后面的代碼相當(dāng)于在異步等待獲取結(jié)果后放到微任務(wù)隊列中相當(dāng)于這樣:Promise.then((res) => {return res}).finally(() => {}), 只有在await 表達式前面獲取到結(jié)果后才會在代碼掛在到異步隊列中
可以做個實驗將上面異步等待定時器的值設(shè)定為更長時間,這個時候await表達式后面的代碼是不為響應(yīng)的,只有獲取到了異步等待的結(jié)果,才會響應(yīng)。
+(res)?=>?{return?res}?//?微4-1
+()?=>?{async1?end}?//?微4-2
宏任務(wù)執(zhí)行完畢開始清空異步任務(wù)中的微任務(wù)隊列:
微4-1:?2?->?微4-2:?async1?end
執(zhí)行微任務(wù)中所產(chǎn)生新的微任務(wù), 放到微任務(wù)隊列中,該微任務(wù)也會在此次輪詢中被清空:
本次微任務(wù)執(zhí)行沒有產(chǎn)生新的微任務(wù)
執(zhí)行微任務(wù)過程中所產(chǎn)生的宏任務(wù)放到新宏任務(wù)隊列中:
本次微任務(wù)執(zhí)行沒有產(chǎn)生新的宏任務(wù)
此次輪詢結(jié)束輸出結(jié)果有:
微4-1:?2?->?微4-2:?async1?end
所有輪詢完畢之后的完整結(jié)果
script?start?->?async1?start?->?error1?->?async2?->?promise1?->?script?end
微1-1:?promise2?->?微1-2:?then?1-1?->?微1-3:?promise3
宏2:setTimeout
微3-1:?promise4?->?微3-2:?undefined
微4-1:?2?->?微4-2:?async1?end
如果你四道題全對的話,那么恭喜你。
寫在最后對于【前端體系】這系列的文章我是抱著很認真,很想寫好的心態(tài)的,但畢竟我還是前端小白&寫作新人,如果文章中有那塊寫的不太好或有問題歡迎大家指出,我也會在后面的文章不停修改。也希望自己進步的同時能跟你們一起成長。喜歡我文章的朋友們也可以關(guān)注一下
我會很感激第一批關(guān)注我的人。此時,年輕的我和你,輕裝上陣;而后,富裕的你和我,滿載而歸。
系列文章
【前端體系】從地基開始打造一座萬丈高樓[8]
參考文章
深入理解 JavaScript Event Loop[9]
參考資料
[1]Processing model: https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model
[2]正在運行的task: https://html.spec.whatwg.org/multipage/webappapis.html#currently-running-task
[3]currently running task: https://html.spec.whatwg.org/multipage/webappapis.html#currently-running-task
[4]microtasks任務(wù)檢查點: https://html.spec.whatwg.org/multipage/webappapis.html#perform-a-microtask-checkpoint
[5]WorkerGlobalScope: https://html.spec.whatwg.org/multipage/workers.html#workerglobalscope
[6]Web workers: https://html.spec.whatwg.org/multipage/workers.html#workers
[7]run a worker: https://html.spec.whatwg.org/multipage/workers.html#run-a-worker
[8]【前端體系】從地基開始打造一座萬丈高樓: https://juejin.im/post/6867784542338416648#comment
[9]深入理解 JavaScript Event Loop: https://zhuanlan.zhihu.com/p/34229323
交流討論
??愛心三連擊
1.看到這里了就點個在看支持下吧,你的「點贊,在看」是我創(chuàng)作的動力。
2.關(guān)注公眾號程序員成長指北,回復(fù)「1」加入Node進階交流群!「在這里有好多 Node 開發(fā)者,會討論 Node 知識,互相學(xué)習(xí)」!
3.也可添加微信【ikoala520】,一起成長。
“在看轉(zhuǎn)發(fā)”是最大的支持
