JS 事件循環(huán) event loop 經(jīng)典面試題含答案

原文地址

掘金

思維導(dǎo)圖

image

一、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í)行順序(很重要)

圖解

  1. 主棧隊(duì)列就是一個(gè)宏任務(wù),每一個(gè)宏任務(wù)執(zhí)行完就會(huì)執(zhí)行宏任務(wù)中的微任務(wù),直到微任務(wù)全部都執(zhí)行完,才開(kāi)始執(zhí)行下一個(gè)宏任務(wù)。
  2. JS 中任務(wù)的執(zhí)行順序優(yōu)先級(jí)是:主棧全局任務(wù)(宏任務(wù)) > 宏任務(wù)中的微任務(wù) > 下一個(gè)宏任務(wù)。,所以 promise(微任務(wù)) 的執(zhí)行順序優(yōu)先級(jí)高于setTimeout定時(shí)器。
  3. 不能滿(mǎn)目的將 .then 的回調(diào)放入微任務(wù)隊(duì)列;因?yàn)闆](méi)有調(diào)用 resolve或者reject 之前是不算異步任務(wù)完成的, 所以不能將回調(diào)隨意的放入微任務(wù)事件隊(duì)列
  4. 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í)行后面的代碼。
  5. process.nextTick 是一個(gè)獨(dú)立于 eventLoop 的任務(wù)隊(duì)列,主棧中的宏任務(wù)每一次結(jié)束后都是先執(zhí)行 process.nextTick隊(duì)列,在執(zhí)行微任務(wù) promise.then()
  6. 每一個(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
*/

任務(wù)隊(duì)列圖解

在主線(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í)行輸出2resolve()異步的丟入微任務(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

  1. 首先開(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ù)。
  2. 開(kāi)始下一輪的宏任務(wù)執(zhí)行第一個(gè)進(jìn)入的 setTimeout,輸出children2,第二輪宏任務(wù)結(jié)束,開(kāi)始微任務(wù)執(zhí)行promise 中的.then() 輸出 children3。第二輪循環(huán)結(jié)束
  3. 接著又開(kāi)始setTimeout 的宏任務(wù),輸出children5,微任務(wù)輸出 children7。這里遇到一個(gè)宏任務(wù) setTimeout,丟入宏任務(wù)隊(duì)列。
  4. 又開(kāi)始新 setTimeout 宏任務(wù),輸出 res children6。

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)注明出處。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容