原文地址
掘金
思維導(dǎo)圖

一、JS異步編程基本概念
JS 之所以是單線(xiàn)程的是因?yàn)闉g覽器(多線(xiàn)程)只分配一個(gè)線(xiàn)程來(lái)執(zhí)行 JS 代碼,之所以只分配一個(gè)線(xiàn)程試因?yàn)闉g覽器考慮到多線(xiàn)程操作會(huì)導(dǎo)致的一些問(wèn)題,
假設(shè) JS 是多線(xiàn)程的,其中一個(gè)線(xiàn)程在 DOM 節(jié)點(diǎn)上添加內(nèi)容,而另一個(gè)線(xiàn)程在這個(gè)節(jié)點(diǎn)上刪除內(nèi)容,那么瀏覽器該執(zhí)行哪一個(gè)呢?所以 JS 的設(shè)計(jì)就是單線(xiàn)程的。但是單線(xiàn)程會(huì)造成很多的任務(wù)都需要等待執(zhí)行,所以就引入了瀏覽器的事件循環(huán)機(jī)制。
進(jìn)程和線(xiàn)程 Tip
進(jìn)程中可以包括多個(gè)線(xiàn)程,比如打開(kāi)一個(gè)頁(yè)面,這個(gè)頁(yè)面就占用了計(jì)算機(jī)的一個(gè)進(jìn)程,頁(yè)面加載時(shí),瀏覽會(huì)分配一個(gè)線(xiàn)程去計(jì)算DOM樹(shù),一個(gè)去執(zhí)行 JS 代碼,其他的線(xiàn)程去加載資源文件等。
二、event loop
JS 主線(xiàn)程不斷的循環(huán)往復(fù)的從任務(wù)隊(duì)列中讀取任務(wù),執(zhí)行任務(wù),這種運(yùn)行機(jī)制稱(chēng)為事件循環(huán)(event loop)。推薦看一個(gè)2分鐘了解event loop
宏任務(wù)和微任務(wù)
瀏覽器的事件循環(huán)(event loop)中分成宏任務(wù)和微任務(wù)。JS 中任務(wù)分成同步任務(wù)和異步任務(wù)。
1. 宏任務(wù)(macro task)
JS 中主棧執(zhí)行的大多數(shù)的任務(wù),例如:定時(shí)器,事件綁定,ajax,回調(diào)函數(shù),node中fs操作模塊等就是宏任務(wù)
2. 微任務(wù)(micro task)
promise, async/await, process.nextTick等就是微任務(wù)。
思考:為什么要引入微任務(wù),只有宏任務(wù)可以嗎?
微任務(wù)的引入是為了解決異步回調(diào)的問(wèn)題,假設(shè)只有宏任務(wù),那么每一個(gè)宏任務(wù)執(zhí)行完后回調(diào)函數(shù)也放入宏任務(wù)隊(duì)列,這樣會(huì)造成隊(duì)列多長(zhǎng),回調(diào)的時(shí)間變長(zhǎng),這樣會(huì)造成頁(yè)面的卡頓,所以引入了微任務(wù)。
思考,為什么 await 后面的代碼會(huì)進(jìn)入到promise隊(duì)列中的微任務(wù)?
async/await 只是操作 promise 的語(yǔ)法糖,最后的本質(zhì)還是promise。舉一個(gè)小栗子
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')
})
}
4. 宏任務(wù)和微任務(wù)的執(zhí)行順序(很重要)
- 主棧隊(duì)列就是一個(gè)宏任務(wù),每一個(gè)宏任務(wù)執(zhí)行完就會(huì)執(zhí)行宏任務(wù)中的微任務(wù),直到微任務(wù)全部都執(zhí)行完,才開(kāi)始執(zhí)行下一個(gè)宏任務(wù)。
- JS 中任務(wù)的執(zhí)行順序優(yōu)先級(jí)是:主棧全局任務(wù)(宏任務(wù)) > 宏任務(wù)中的微任務(wù) > 下一個(gè)宏任務(wù)。,所以
promise(微任務(wù))的執(zhí)行順序優(yōu)先級(jí)高于setTimeout定時(shí)器。- 不能滿(mǎn)目的將
.then的回調(diào)放入微任務(wù)隊(duì)列;因?yàn)闆](méi)有調(diào)用resolve或者reject之前是不算異步任務(wù)完成的, 所以不能將回調(diào)隨意的放入微任務(wù)事件隊(duì)列await是一個(gè)讓出線(xiàn)程的標(biāo)志。await后面的表達(dá)式會(huì)先執(zhí)行一遍,將await后面的代碼加入到micro task中這個(gè)微任務(wù)是promise隊(duì)列中微任務(wù),然后就會(huì)跳出整個(gè)async函數(shù)來(lái)繼續(xù)執(zhí)行后面的代碼。process.nextTick是一個(gè)獨(dú)立于eventLoop的任務(wù)隊(duì)列,主棧中的宏任務(wù)每一次結(jié)束后都是先執(zhí)行process.nextTick隊(duì)列,在執(zhí)行微任務(wù)promise的.then()。- 每一個(gè)宏任務(wù)和宏任務(wù)的微任務(wù)執(zhí)行完后都會(huì)對(duì)頁(yè)面 UI 進(jìn)行渲染。
熱身1 先看一個(gè)小栗子
// A 任務(wù)
setTimeout(() => {
console.log(1)
}, 20)
// B 任務(wù)
setTimeout(() => {
console.log(2)
}, 0)
// C 任務(wù)
setTimeout(() => {
console.log(3)
}, 10)
// D
setTimeout(() => {
console.log(5)
}, 10)
console.log(4)
/* 輸出
* 4 -> 2-> 3 -> 5 -> 1
*/
在主線(xiàn)程的主任務(wù)(宏任務(wù))先自上而下執(zhí)行,遇到
setTimeout代碼都是下一個(gè)(宏任務(wù))。所以都會(huì)被加入到等待隊(duì)列中,瀏覽器有專(zhuān)門(mén)監(jiān)聽(tīng)等待隊(duì)列中的代碼,在主棧中的同步代碼執(zhí)行完成后,等待隊(duì)列中的任務(wù)先到執(zhí)行時(shí)間的就先執(zhí)行,如果等待任務(wù)隊(duì)列中有兩個(gè)同時(shí)到執(zhí)行時(shí)間的異步代碼,那么先入隊(duì)列的就先到主棧中執(zhí)行。所以等待隊(duì)列中 B 任務(wù)執(zhí)行后到 C 任務(wù)到 D 任務(wù) 再到 A 任務(wù)。輸出的結(jié)果就是4 -> 2-> 3 -> 1。
熱身2 將上面的栗子改一下
// A 任務(wù)
setTimeout(() => {
console.log(1)
}, 20)
// B 任務(wù)
setTimeout(() => {
console.log(2)
}, 0)
// C 任務(wù)
setTimeout(() => {
console.log(3)
}, 30)
console.log(4)
/* 輸出
* 4 -> 2-> 1 -> 3
*/
這題的原理和上面一樣,任務(wù)A 的執(zhí)行時(shí)間比 C 任務(wù)先到了就先輸出了 1 后輸出 3。
三、思考題求輸出順序(chrome 瀏覽器為準(zhǔn))
1. 來(lái)一道思考題,求輸出結(jié)果
let xhr = new XMLHttpRequest()
xhr.open('post', 'api')
xhr.onreadystatechange = () =>{
if(xhr.readyState == 2){
console.log(2)
}
if(xhr.readyState == 4){
console.log(4)
}
}
xhr.send()
console.log(3)
/* 輸出
* 3 2 4
*/
2. 再來(lái)一道思考題,在同步請(qǐng)求中下面代碼輸出的是什么
let xhr = new XMLHttpRequest()
xhr.open('get', 'xxx', false)
xhr.send()
xhr.onreadystatechange = () => {
console.log(xhr.readyState)
}
沒(méi)有輸出,上面的兩道題在 面試 | Ajax,fetch,axios的超高頻面試題 有解析。
3. 一道 Ajax 異步思考題,求輸出結(jié)果。
let xhr = new XMLHttpRequest()
xhr.open('post', 'api')
xhr.onreadystatechange = () =>{
console.log(xhr.readyState)
}
xhr.send()
/* 輸出
* 2 -> 3 -> 4。
*/
xhr.onreadystatechange是異步的會(huì)加入到等待隊(duì)列,主任務(wù)執(zhí)行xhr.send()后 ajax 的狀態(tài)碼變成1。主任務(wù)空閑等待任務(wù)中的xhr.onreadystatechange開(kāi)始監(jiān)聽(tīng)到狀態(tài)碼變化,知道狀態(tài)碼由2 -> 3 -> 4后不再變化。如果不熟悉 ajax 狀態(tài)碼的可以看看 面試 | Ajax,fetch,axios的超高頻面試題。
4. promise 熱身題,求輸出結(jié)果
console.log(1)
new Promise((resolve, reject) => {
console.log(2)
resolve()
}).then(res => {
console.log(3)
})
console.log(4)
/* 輸出
* 1 -> 2 -> 4 ->3
*/
解答:第一輪宏任務(wù)就是主棧中的同步任務(wù),先輸出
1,JS 代碼執(zhí)行到promise立即執(zhí)行輸出2,resolve將.then()中的代碼放入到微任務(wù)隊(duì)列,宏任務(wù)結(jié)束后輸出4,最后執(zhí)行微任務(wù)隊(duì)列輸出3
4. setTimeout 和 Promise 的執(zhí)行順序
setTimeout(function () {
console.log(1)
}, 0);
new Promise(function (resolve, reject) {
console.log(2);
resolve();
}).then(function () {
console.log(3)
}).then(function () {
console.log(4)
});
console.log(6);
// 2, 6, 3, 4, 1
解答:先開(kāi)始主棧中的宏任務(wù),遇到
setTimeout后丟入宏任務(wù)隊(duì)列等待,遇到promise立即執(zhí)行輸出2,resolve()異步的丟入微任務(wù)隊(duì)列,最后輸出6,第一個(gè)宏任務(wù)執(zhí)行結(jié)束開(kāi)始留下來(lái)的微任務(wù),即.then()輸出3, 4。第一輪循環(huán)結(jié)束開(kāi)始下一輪宏任務(wù)setTimeout,輸出1。
5. setTimeout 和 Promise 的執(zhí)行順序
setTimeout(function () {
console.log(1)
}, 0);
new Promise(function (resolve, reject) {
console.log(2)
for (var i = 0; i < 10000; i++) {
if (i === 10) {
console.log(10)
}
i == 9999 && resolve();
}
console.log(3)
}).then(function () {
console.log(4)
})
console.log(5);
// 2, 10, 3, 5, 4, 1
這道題的解法和上面相同,都需要區(qū)分宏任務(wù)和微任務(wù)。
6.求輸出結(jié)果
console.log("start");
setTimeout(() => {
console.log("children2")
Promise.resolve().then(() =>{
console.log("children3")
})
}, 0)
new Promise(function(resolve, reject){
console.log("children4")
setTimeout(function(){
console.log("children5")
resolve("children6")
}, 0)
}).then(res =>{ // flag
console.log("children7")
setTimeout(() =>{
console.log(res)
}, 0)
})
// start children4 children2 children3 children5 children7 children6
- 首先開(kāi)始主任務(wù)中的第一輪宏任務(wù),輸出
start,遇到setTimeout不需要等待 0s 而是直接丟入宏任務(wù)隊(duì)列(有人說(shuō)需要等待 0s再放入到任務(wù)隊(duì)列是不對(duì)的,可以使用console.time/timeEnd來(lái)測(cè)試),遇到promise立即執(zhí)行輸出children4,又遇到一個(gè)setTimeout直接又丟入到宏任務(wù)隊(duì)列,第一輪宏任務(wù)執(zhí)行完,且沒(méi)有微任務(wù)。問(wèn):上面的.then()(注釋的flag處) 是第一輪宏任務(wù)循環(huán)的微任務(wù)嗎?不是!因?yàn)?code>resolve 都沒(méi)有執(zhí)行,promise的狀態(tài)都還沒(méi)有從pending改變,就不是第一輪的微任務(wù)。- 開(kāi)始下一輪的宏任務(wù)執(zhí)行第一個(gè)進(jìn)入的
setTimeout,輸出children2,第二輪宏任務(wù)結(jié)束,開(kāi)始微任務(wù)執(zhí)行promise中的.then()輸出children3。第二輪循環(huán)結(jié)束- 接著又開(kāi)始
setTimeout的宏任務(wù),輸出children5,微任務(wù)輸出children7。這里遇到一個(gè)宏任務(wù)setTimeout,丟入宏任務(wù)隊(duì)列。- 又開(kāi)始新
setTimeout宏任務(wù),輸出 reschildren6。
7. (頭條)請(qǐng)寫(xiě)出下面代碼的運(yùn)行結(jié)果(不同的環(huán)境下輸出有差異,下面以最新的 Chromium 為準(zhǔn))
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((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
這道題的難點(diǎn)在于是
promise2還是async1 end先輸出。從全局宏任務(wù)之上而下執(zhí)行時(shí)await async2()后面的代碼console.log('async1 end')先進(jìn)入promise中的微任務(wù)隊(duì)列,最后.then()中的console.log('promise2')再進(jìn)入到promise中的微任務(wù)隊(duì)列。所以再開(kāi)始下一輪宏任務(wù)循環(huán)之前先輸出了async1 end再輸出了promise2。全局中的微任務(wù)執(zhí)行完成開(kāi)始下一輪宏任務(wù)setTimeout最后輸出setTimeout。
8. 將上一道題目變換一下,求輸出(不同的環(huán)境下輸出有差異,下面以最新的 Chromium 為準(zhǔn))
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function 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
首先開(kāi)始全局下的宏任務(wù)依次輸出 script start, async1 start, promise1, promise3, script end。其中
await async2();,async2()中.then()的代碼先進(jìn)入到promise的微任務(wù)隊(duì)列,await async2();后面的代碼再進(jìn)入到promise的任務(wù)隊(duì)列,console.log('promise4');最后進(jìn)入到promise的任務(wù)隊(duì)列。全局下的宏任務(wù)結(jié)束,開(kāi)始全局下的微任務(wù),promise 的微任務(wù)隊(duì)列中按照隊(duì)列的先進(jìn)先出原則依次輸出,promise2,async1 end,promise4。全局微任務(wù)結(jié)束,開(kāi)始下一輪的宏任務(wù)setTimeout,最終輸出setTimeout。
9. 再來(lái)將上面的題目變換一下,求輸出(不同的環(huán)境下輸出有差異,下面以最新的 Chromium 為準(zhǔn))
async function async1() {
console.log('async1 start');
await async2();
setTimeout(function() {
console.log('setTimeout1') // 這一部分代碼會(huì)放入到 promise 的微任務(wù)隊(duì)列中。
},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
按照上面的解析,原理都是一樣的,全局下的宏任務(wù)執(zhí)行完成后,開(kāi)始執(zhí)行全局下的微任務(wù)
.then()中的代碼,最后開(kāi)始下一輪宏任務(wù)的執(zhí)行,下一輪宏任務(wù)是setTimeout3先執(zhí)行,因?yàn)槭?code>setTimeout3 先加入下一個(gè)宏任務(wù)隊(duì)列中的,再依次加入setTimeout2, setTimeout1到宏任務(wù)隊(duì)列。所以輸出的結(jié)果是setTimeout3, setTimeout2, setTimeout1
參考
《Javascript 忍者秘籍》第二版,事件循環(huán)篇
第 10 題:常見(jiàn)異步筆試題,請(qǐng)寫(xiě)出代碼的運(yùn)行結(jié)果
結(jié)束
js 異步隊(duì)列的題目就先到這里,如果覺(jué)得不過(guò)癮的可以看看這篇文章的面試 | JS 你不得不懂的 異步編程 | promise 篇超高頻面試題面試題
作者:林一一呢
鏈接:http://www.itdecent.cn/p/5a4b11c071ab
來(lái)源:簡(jiǎn)書(shū)
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處。