😄😄廢話很少說,先上題:html
//請寫出輸出內容 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 */
這道題主要考察的是事件循環中函數執行順序的問題,其中包括`async/await`,`setTimeout`,`Promise`函數。下面來講一下本題中涉及到的知識點。html5
首先咱們須要明白如下幾件事情:git
* JS分爲同步任務和異步任務
* 同步任務都在主線程上執行,造成一個執行棧
* 主線程以外,事件觸發線程管理着一個任務隊列,只要異步任務有了運行結果,就在任務隊列之中放置一個事件。
* 一旦執行棧中的全部同步任務執行完畢(此時JS引擎空閒),系統就會讀取任務隊列,將可運行的異步任務添加到可執行棧中,開始執行。github
根據規範,事件循環是經過[任務隊列](https://www.w3.org/TR/html5/webappapis.html#task-queues)的機制來進行協調的。web
一個 Event Loop 中,能夠有一個或者多個任務隊列(task queue),一個任務隊列即是一系列有序任務(task)的集合;每一個任務都有一個任務源(task source),源自同一個任務源的 task 必須放到同一個任務隊列,從不一樣源來的則被添加到不一樣隊列。api
setTimeout/Promise 等API即是任務源,而進入任務隊列的是他們指定的具體執行任務。promise
(macro)task(又稱之爲宏任務),能夠理解是每次執行棧執行的代碼就是一個宏任務(包括每次從事件隊列中獲取一個事件回調並放到執行棧中執行)。瀏覽器
瀏覽器爲了可以使得JS內部(macro)task與DOM任務可以有序的執行,會在一個(macro)task執行結束後,在下一個(macro)task 執行開始前,對頁面進行從新渲染,流程以下:app
(macro)task->渲染->(macro)task->...
(macro)task主要包含:script(總體代碼)、setTimeout、setInterval、I/O、UI交互事件、postMessage、MessageChannel、setImmediate(Node.js 環境)webapp
microtask(又稱爲微任務),能夠理解是在當前 task 執行結束後當即執行的任務。也就是說,在當前task任務後,下一個task以前,在渲染以前。
因此它的響應速度相比setTimeout(setTimeout是task)會更快,由於無需等渲染。也就是說,在某一個macrotask執行完後,就會將在它執行期間產生的全部microtask都執行完畢(在渲染前)。
microtask主要包含:Promise.then、MutaionObserver、process.nextTick(Node.js 環境)
在事件循環中,每進行一次循環操做稱爲 tick,每一次 tick 的任務[處理模型](https://www.w3.org/TR/html5/webappapis.html#event-loops-processing-model)是比較複雜的,但關鍵步驟以下:
* 執行一個宏任務(棧中沒有就從事件隊列中獲取)
* 執行過程當中若是遇到微任務,就將它添加到微任務的任務隊列中
* 宏任務執行完畢後,當即執行當前微任務隊列中的全部微任務(依次執行)
* 當前宏任務執行完畢,開始檢查渲染,而後GUI線程接管渲染
* 渲染完畢後,JS線程繼續接管,開始下一個宏任務(從事件隊列中獲取)
流程圖以下:
咱們知道Promise中的異步體如今`then`和`catch`中,因此寫在Promise中的代碼是被當作同步任務當即執行的。而在async/await中,在出現await出現以前,其中的代碼也是當即執行的。那麼出現了await時候發生了什麼呢?
從字面意思上看await就是等待,await 等待的是一個表達式,這個表達式的返回值能夠是一個promise對象也能夠是其餘值。
不少人覺得await會一直等待以後的表達式執行完以後纔會繼續執行後面的代碼,實際上await是一個讓出線程的標誌。await後面的表達式會先執行一遍,將await後面的代碼加入到microtask中,而後就會跳出整個async函數來執行後面的代碼。(其中對於紅色部分文字我是存疑的)
能夠看到issue中有人提到,因爲由於async await 自己就是promise+generator的語法糖。因此await後面的代碼是microtask。因此對於本題中的
async function async1() { console.log('async1 start'); await async2(); console.log('async1 end'); }
等價於
async function async1() { console.log('async1 start'); Promise.resolve(async2()).then(() => { console.log('async1 end'); }) }
接着往下看到issue中,又有人指出這裏的`console.log('async1 end')`是屬於macrotask中的script隊列中的,由於script隊列在setTimout隊列前面,因此會比setTimout先輸出。也能夠理解爲是同步代碼,因此先輸出。
因此這裏有兩種說法,我表示是有疑問的......🤔️🤔️
結合這篇文章 https://juejin.im/post/5c148ec8e51d4576e83fd836 中的例子🌰,說說async/await的運行機制。
根據個人觀察,這裏我大膽的作個結論:
一、當await後面爲非promise時,那麼當外部同步代碼執行完後,若是外部Promise執行中resolve的調用帶參數,那麼此時await下面的代碼先於外部Promise回調入隊的微任務執行。
若外部Promise執行時resolve調用不帶參數,那麼外部Promise回調入隊的微任務先於await後面的代碼執行。
你確定會問:Why?
* 根據 [Promises/A+規範](http://www.ituring.com.cn/article/66566):
`Promise.resolve` 方法容許調用時不帶參數,直接返回一個`resolved` 狀態的 `Promise` 對象。當即 `resolved` 的 `Promise` 對象,是在本輪「事件循環」(event loop)的結束時,而不是在下一輪「事件循環」的開始時。
對於Promise.resolve: 若是參數是個非 thenable 對象或者不是一個對象,也是返回一個 `resolved` 狀態的 Promise。
因此,例如:
new Promise(resolve => { resolve(1); Promise.resolve().then(() => { // t2 console.log(2) }); console.log(4) }).then(t => { // t1 console.log(t) }); console.log(3);
這段代碼,結果爲4321。
二、當await後面爲promise時,那麼Promise的回調入隊的微任務將先於await下面的代碼執行。
以上就本道題涉及到的全部相關知識點了,下面咱們再回到這道題來一步一步看看怎麼回事兒。
1. 首先,事件循環從宏任務(macrotask)隊列開始,這個時候,宏任務隊列中,只有一個script(總體代碼)任務;當遇到任務源(task source)時,則會先分發任務到對應的任務隊列中去。
2. 而後咱們看到首先定義了兩個async函數,接着往下看,而後遇到了 `console` 語句,直接輸出 `script start`。輸出以後,script 任務繼續往下執行,遇到 `setTimeout`,其做爲一個宏任務源,則會先將其任務分發到對應的隊列中。
3. script 任務繼續往下執行,執行了async1()函數,前面講過async函數中在await以前的代碼是當即執行的,因此會當即輸出`async1 start`。
遇到了await時,會將await後面的表達式執行一遍,因此就緊接着輸出`async2`,而後將await後面的代碼也就是`console.log('async1 end')`加入到microtask中的Promise隊列中,接着跳出async1函數來執行後面的代碼。
4. script任務繼續往下執行,遇到Promise實例。因爲Promise中的函數是當即執行的,然後續的 `.then` 則會被分發到 microtask 的 `Promise` 隊列中去。因此會先輸出 `promise1`,而後執行 `resolve`,將 `promise2` 分配到對應隊列。
5. script任務繼續往下執行,最後只有一句輸出了 `script end`,至此,全局任務就執行完畢了。
根據上述,每次執行完一個宏任務以後,會去檢查是否存在 Microtasks;若是有,則執行 Microtasks 直至清空 Microtask Queue。
於是在script任務執行完畢以後,開始查找清空微任務隊列。此時,微任務中, `Promise` 隊列有的兩個任務`async1 end`和`promise2`,所以按前後順序輸出 `async1 end,promise2`。當全部的 Microtasks 執行完畢以後,表示第一輪的循環就結束了。
6. 第二輪循環依舊從宏任務隊列開始。此時宏任務中只有一個 `setTimeout`,取出直接輸出便可,至此整個流程結束。
下面我會改變一下代碼來加深印象。
在第一個變式中將async2中的函數也變成了Promise函數,代碼以下:
async function async1() { console.log('async1 start'); await async2(); console.log('async1 end'); } async function async2() { //async2作出以下更改: new Promise(function(resolve) { console.log('promise1'); resolve(); }).then(function() { console.log('promise2'); }); } console.log('script start'); setTimeout(function() { console.log('setTimeout'); }, 0) async1(); new Promise(function(resolve) { console.log('promise3'); resolve(); }).then(function() { console.log('promise4'); }); console.log('script end');
輸出結果:
/* script start async1 start promise1 promise3 script end promise2 async1 end
promis4 setTimeout */
在第一次macrotask執行完以後,也就是輸出`script end`以後,會去清理全部microtask。因此會相繼輸出`promise2`, ` async1 end`,`promise4`。
在第二個變式中,將async1中await後面的代碼和async2的代碼都改成異步的,代碼以下:
async function async1() { console.log('async1 start'); await async2(); //更改以下: setTimeout(function() { console.log('setTimeout1') },0) } async function async2() { //更改以下: setTimeout(function() { console.log('setTimeout2') },0) } console.log('script start'); setTimeout(function() { console.log('setTimeout3'); }, 0) async1(); new Promise(function(resolve) { console.log('promise1'); resolve(); }).then(function() { console.log('promise2'); }); console.log('script end');
輸出結果:
/* script start async1 start promise1 script end promise2 setTimeout3 setTimeout2 setTimeout1 */
在輸出爲`promise2`以後,接下來會按照加入setTimeout隊列的順序來依次輸出,經過代碼咱們能夠看到加入順序爲`3 2 1`,因此會按3,2,1的順序來輸出。
代碼以下:
async function a1 () { console.log('a1 start') await a2() console.log('a1 end') } async function a2 () { console.log('a2') } console.log('script start') setTimeout(() => { console.log('setTimeout') }, 0) Promise.resolve().then(() => { console.log('promise1') }) a1() let promise2 = new Promise((resolve) => { resolve('promise2.then') console.log('promise2') }) promise2.then((res) => { console.log(res) Promise.resolve().then(() => { console.log('promise3') }) }) console.log('script end')
輸出結果:
/* script start a1 start a2 promise2 script end promise1 a1 end promise2.then promise3 setTimeout */
原文地址:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/7