原文鏈接: https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/7
注:本篇文章運行環(huán)境為當前最新版本的谷歌瀏覽器(72.0.3626.109)
任務隊列
首先我們需要明白以下幾件事情:
- JS分為同步任務和異步任務
- 同步任務都在主線程上執(zhí)行涝缝,形成一個執(zhí)行棧
- 主線程之外您觉,事件觸發(fā)線程管理著一個任務隊列,只要異步任務有了運行結果逆趋,就在任務隊列之中放置一個事件蒙揣。
- 一旦執(zhí)行棧中的所有同步任務執(zhí)行完畢(此時JS引擎空閑),系統(tǒng)就會讀取任務隊列,將可運行的異步任務添加到可執(zhí)行棧中今瀑,開始執(zhí)行。
根據規(guī)范点把,事件循環(huán)是通過任務隊列的機制來進行協(xié)調的橘荠。一個 Event Loop 中,可以有一個或者多個任務隊列(task queue)郎逃,一個任務隊列便是一系列有序任務(task)的集合哥童;每個任務都有一個任務源(task source),源自同一個任務源的 task 必須放到同一個任務隊列褒翰,從不同源來的則被添加到不同隊列贮懈。 setTimeout/Promise 等API便是任務源,而進入任務隊列的是他們指定的具體執(zhí)行任務优训。
宏任務
(macro)task(又稱之為宏任務)朵你,可以理解是每次執(zhí)行棧執(zhí)行的代碼就是一個宏任務(包括每次從事件隊列中獲取一個事件回調并放到執(zhí)行棧中執(zhí)行)。
瀏覽器為了能夠使得JS內部(macro)task與DOM任務能夠有序的執(zhí)行揣非,會在一個(macro)task執(zhí)行結束后抡医,在下一個(macro)task 執(zhí)行開始前,對頁面進行重新渲染早敬,流程如下:
(macro)task->渲染->(macro)task->...
(macro)task主要包含:script(整體代碼)忌傻、setTimeout、setInterval搁嗓、I/O仓洼、UI交互事件统刮、postMessage、MessageChannel、setImmediate(Node.js 環(huán)境)
微任務
microtask(又稱為微任務)艺沼,可以理解是在當前 task 執(zhí)行結束后立即執(zhí)行的任務够庙。也就是說伍伤,在當前task任務后乳蛾,下一個task之前,在渲染之前够委。
所以它的響應速度相比setTimeout(setTimeout是task)會更快荐类,因為無需等渲染。也就是說茁帽,在某一個macrotask執(zhí)行完后玉罐,就會將在它執(zhí)行期間產生的所有microtask都執(zhí)行完畢(在渲染前)屈嗤。
microtask主要包含:Promise.then、MutaionObserver吊输、process.nextTick(Node.js 環(huán)境)
運行機制
在事件循環(huán)中饶号,每進行一次循環(huán)操作稱為 tick,每一次 tick 的任務處理模型是比較復雜的季蚂,但關鍵步驟如下:
- 執(zhí)行一個宏任務(棧中沒有就從事件隊列中獲让4)
- 執(zhí)行過程中如果遇到微任務,就將它添加到微任務的任務隊列中
- 宏任務執(zhí)行完畢后扭屁,立即執(zhí)行當前微任務隊列中的所有微任務(依次執(zhí)行)
- 當前宏任務執(zhí)行完畢算谈,開始檢查渲染,然后GUI線程接管渲染
- 渲染完畢后料滥,JS線程繼續(xù)接管然眼,開始下一個宏任務(從事件隊列中獲取)
流程圖如下:
Promise和async中的立即執(zhí)行
我們知道Promise中的異步體現在then
和catch
中葵腹,所以寫在Promise中的代碼是被當做同步任務立即執(zhí)行的罪治。而在async/await中,在出現await出現之前礁蔗,其中的代碼也是立即執(zhí)行的。那么出現了await時候發(fā)生了什么呢雁社?
await做了什么
從字面意思上看await就是等待浴井,await 等待的是一個表達式,這個表達式的返回值可以是一個promise對象也可以是其他值霉撵。
很多人以為await會一直等待之后的表達式執(zhí)行完之后才會繼續(xù)執(zhí)行后面的代碼磺浙,實際上await是一個讓出線程的標志。await后面的表達式會先執(zhí)行一遍徒坡,將await后面的代碼加入到microtask中撕氧,然后就會跳出整個async函數來執(zhí)行后面的代碼。
這里感謝@chenjigeng的糾正:
由于因為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');
})
}
回到本題
//請寫出輸出內容
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start'); // 1
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
*/
這道題主要考察的是事件循環(huán)中函數執(zhí)行順序的問題,其中包括async
锦溪,await
不脯,setTimeout
,Promise
函數刻诊。下面來說一下本題中涉及到的知識點防楷。
以上就本道題涉及到的所有相關知識點了,下面我們再回到這道題來一步一步看看怎么回事兒则涯。
-
首先复局,事件循環(huán)從宏任務(macrotask)隊列開始冲簿,這個時候,宏任務隊列中亿昏,只有一個script(整體代碼)任務峦剔;當遇到任務源(task source)時,則會先分發(fā)任務到對應的任務隊列中去龙优。所以羊异,上面例子的第一步執(zhí)行如下圖所示:
[圖片上傳失敗...(image-fd7cde-1584671574376)]
然后我們看到首先定義了兩個async函數,接著往下看彤断,然后遇到了
console
語句野舶,直接輸出script start
。輸出之后宰衙,script 任務繼續(xù)往下執(zhí)行平道,遇到setTimeout
,其作為一個宏任務源供炼,則會先將其任務分發(fā)到對應的隊列中:
[圖片上傳失敗...(image-c2e621-1584671574376)]-
script 任務繼續(xù)往下執(zhí)行一屋,執(zhí)行了async1()函數,前面講過async函數中在await之前的代碼是立即執(zhí)行的袋哼,所以會立即輸出
async1 start
冀墨。
遇到了await時,會將await后面的表達式執(zhí)行一遍涛贯,所以就緊接著輸出async2
诽嘉,然后將await后面的代碼也就是console.log('async1 end')
加入到microtask中的Promise隊列中,接著跳出async1函數來執(zhí)行后面的代碼弟翘。
-
script任務繼續(xù)往下執(zhí)行虫腋,遇到Promise實例。由于Promise中的函數是立即執(zhí)行的稀余,而后續(xù)的
.then
則會被分發(fā)到 microtask 的Promise
隊列中去悦冀。所以會先輸出promise1
,然后執(zhí)行resolve
睛琳,將promise2
分配到對應隊列盒蟆。
script任務繼續(xù)往下執(zhí)行,最后只有一句輸出了
script end
师骗,至此茁影,全局任務就執(zhí)行完畢了。
根據上述丧凤,每次執(zhí)行完一個宏任務之后募闲,會去檢查是否存在 Microtasks;如果有愿待,則執(zhí)行 Microtasks 直至清空 Microtask Queue浩螺。
因而在script任務執(zhí)行完畢之后靴患,開始查找清空微任務隊列。此時要出,微任務中鸳君,Promise
隊列有的兩個任務async1 end
和promise2
,因此按先后順序輸出async1 end患蹂,promise2
或颊。當所有的 Microtasks 執(zhí)行完畢之后,表示第一輪的循環(huán)就結束了传于。第二輪循環(huán)開始囱挑,這個時候就會跳回async1函數中執(zhí)行后面的代碼,然后遇到了同步任務
console
語句沼溜,直接輸出async1 end
平挑。這樣第二輪的循環(huán)就結束了。(也可以理解為被加入到script任務隊列中系草,所以會先與setTimeout隊列執(zhí)行)第二輪循環(huán)依舊從宏任務隊列開始通熄。此時宏任務中只有一個
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
promise4
setTimeout
在第一次macrotask執(zhí)行完之后,也就是輸出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