一道有關(guān)事件循環(huán)的前端面試題:
//請(qǐng)寫出輸出內(nèi)容
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
*/
這道題主要考察的是事件循環(huán)中函數(shù)執(zhí)行順序的問題秀仲,其中包括async
,await
球凰,setTimeout
阀蒂,Promise
函數(shù)该窗。主要涉及到以下知識(shí)點(diǎn)。
任務(wù)隊(duì)列
- JS分為同步任務(wù)和異步任務(wù)
- 同步任務(wù)都在主線程上執(zhí)行蚤霞,形成一個(gè)執(zhí)行棧
- 主線程之外,事件觸發(fā)線程管理著一個(gè)任務(wù)隊(duì)列(包括 宏任務(wù)隊(duì)列 和 微任務(wù)隊(duì)列)义钉,只要異步任務(wù)有了運(yùn)行結(jié)果昧绣,就在任務(wù)隊(duì)列之中放置一個(gè)事件。
-
一旦執(zhí)行棧中的所有同步任務(wù)執(zhí)行完畢(此時(shí)JS引擎空閑)捶闸,系統(tǒng)就會(huì)讀取任務(wù)隊(duì)列夜畴,將可運(yùn)行的異步任務(wù)添加到可執(zhí)行棧中,開始執(zhí)行
宏任務(wù)
task(又稱之為宏任務(wù))删壮,可以理解是每次執(zhí)行棧執(zhí)行的代碼就是一個(gè)宏任務(wù)(包括每次從事件隊(duì)列中獲取一個(gè)事件回調(diào)并放到執(zhí)行棧中執(zhí)行)贪绘。
瀏覽器為了能夠使得JS內(nèi)部task與DOM任務(wù)能夠有序的執(zhí)行,會(huì)在一個(gè)task執(zhí)行結(jié)束后央碟,在下一個(gè)(macro)task 執(zhí)行開始前税灌,對(duì)頁(yè)面進(jìn)行重新渲染,流程如下:
task主要包含:script(整體代碼)亿虽、setTimeout菱涤、setInterval、I/O洛勉、UI交互事件粘秆、postMessage、MessageChannel收毫、setImmediate(Node.js 環(huán)境)
微任務(wù)
microtask(又稱為微任務(wù))攻走,可以理解是在當(dāng)前 task 執(zhí)行結(jié)束后立即執(zhí)行的任務(wù)。也就是說此再,在當(dāng)前task任務(wù)后昔搂,下一個(gè)task之前,在渲染之前引润。
所以它的響應(yīng)速度相比setTimeout(setTimeout是task)會(huì)更快巩趁,因?yàn)闊o(wú)需等渲染。也就是說淳附,在某一個(gè)macrotask執(zhí)行完后议慰,就會(huì)將在它執(zhí)行期間產(chǎn)生的所有microtask都執(zhí)行完畢(在渲染前)。
microtask主要包含:Promise.then奴曙、MutaionObserver别凹、process.nextTick(Node.js 環(huán)境)
運(yùn)行機(jī)制
在事件循環(huán)中,每進(jìn)行一次循環(huán)操作稱為 tick洽糟,每一次 tick 的任務(wù)處理模型是比較復(fù)雜的炉菲,但關(guān)鍵步驟如下:
- 執(zhí)行一個(gè)宏任務(wù)(棧中沒有就從事件隊(duì)列中獲榷檎健)
- 執(zhí)行過程中如果遇到微任務(wù),就將它添加到微任務(wù)的任務(wù)隊(duì)列中
- 宏任務(wù)執(zhí)行完畢后拍霜,立即執(zhí)行當(dāng)前微任務(wù)隊(duì)列中的所有微任務(wù)(依次執(zhí)行)
- 當(dāng)前宏任務(wù)執(zhí)行完畢嘱丢,開始檢查渲染,然后GUI線程接管渲染
- 渲染完畢后祠饺,JS線程繼續(xù)接管越驻,開始下一個(gè)宏任務(wù)(從事件隊(duì)列中獲取)
流程圖如下:
簡(jiǎn)單的說就是按照程序順序執(zhí)行道偷,遇到微任務(wù)就放到放到微任務(wù)隊(duì)列缀旁,遇到宏任務(wù)就放到宏任務(wù)隊(duì)列,執(zhí)行棧執(zhí)行完之后勺鸦,再清空微任務(wù)隊(duì)列并巍,接著執(zhí)行宏任務(wù)隊(duì)列
Promise和async中的立即執(zhí)行
我們知道Promise中的異步體現(xiàn)在then
和catch
中,所以寫在Promise中的代碼是被當(dāng)做同步任務(wù)立即執(zhí)行的换途。而在async/await中懊渡,在出現(xiàn)await出現(xiàn)之前,其中的代碼也是立即執(zhí)行的怀跛。那么出現(xiàn)了await時(shí)候發(fā)生了什么呢距贷?
await做了什么
很多人以為await會(huì)一直等待之后的表達(dá)式執(zhí)行完之后才會(huì)繼續(xù)執(zhí)行后面的代碼,實(shí)際上await是一個(gè)讓出線程的標(biāo)志吻谋。await后面的表達(dá)式會(huì)先執(zhí)行一遍忠蝗,將await后面的代碼加入到microtask中,然后就會(huì)跳出整個(gè)async函數(shù)來執(zhí)行后面的代碼漓拾。
由于因?yàn)閍sync await 本身就是promise+generator的語(yǔ)法糖阁最。所以await后面的代碼是microtask。所以對(duì)于本題中的
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
等價(jià)于
async function async1() {
console.log('async1 start');
Promise.resolve(async2()).then(() => {
console.log('async1 end');
})
}
看一個(gè)使用 await
的例子:
let a = 0
let b = async () => {
a = a + await 10
console.log('2', a) // -> '2' 10
}
b()
a++
console.log('1', a) // -> '1' 1
對(duì)于以上代碼你可能會(huì)有疑惑骇两,讓我來解釋下原因
- 首先函數(shù)
b
先執(zhí)行速种,在執(zhí)行到await 10
之前變量a
還是 0,因?yàn)?await
內(nèi)部實(shí)現(xiàn)了generator
低千,generator
會(huì)保留堆棧中東西配阵,所以這時(shí)候a = 0
被保存了下來 - 因?yàn)?
await
是異步操作,后來的表達(dá)式不返回Promise
的話示血,就會(huì)包裝成Promise.reslove(返回值)
棋傍,然后會(huì)去執(zhí)行函數(shù)外的同步代碼 - 同步代碼執(zhí)行完畢后開始執(zhí)行異步代碼,將保存下來的值拿出來使用难审,這時(shí)候
a = 0 + 10
下面的題目有助于加深對(duì)于javascript事件循環(huán)的理解
變式一
在第一個(gè)變式中我將async2中的函數(shù)也變成了Promise函數(shù)瘫拣,代碼如下:
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');
可以先自己看看輸出順序會(huì)是什么,下面來公布結(jié)果:
script start
async1 start
promise1
promise3
script end
promise2
async1 end
promise4
setTimeout
變式二
在第二個(gè)變式中告喊,我將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');
可以先自己看看輸出順序會(huì)是什么派昧,下面來公布結(jié)果:
script start
async1 start
promise1
script end
promise2
setTimeout3
setTimeout2
setTimeout1
變式三
變式三是我在一篇面經(jīng)中看到的原題,整體來說大同小異拢切,代碼如下:
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')
無(wú)非是在微任務(wù)那塊兒做點(diǎn)文章蒂萎,前面的內(nèi)容如果你都看懂了的話這道題一定沒問題的,結(jié)果如下:
script start
a1 start
a2
promise2
script end
promise1
a1 end
promise2.then
promise3
setTimeout
參考文章