JavaScript之徹底理解 EventLoop
點(diǎn)擊上方?前端瓶子君,關(guān)注公眾號(hào)
回復(fù)算法,加入前端編程面試算法每日一題群

前言
Event Loop即事件循環(huán),是瀏覽器或Node解決單線程運(yùn)行時(shí)不會(huì)阻塞的一種機(jī)制。
在正式學(xué)習(xí)Event Loop之前,先需要解決幾個(gè)問題:
什么是同步與異步? JavaScript是一門單線程語言,那如何實(shí)現(xiàn)異步?同步任務(wù)和異步任務(wù)的執(zhí)行順序如何? 異步任務(wù)是否存在優(yōu)先級(jí)?
同步與異步
計(jì)算機(jī)領(lǐng)域中的同步與異步和我們現(xiàn)實(shí)社會(huì)的同步和異步正好相反。現(xiàn)實(shí)中的同步,就是同時(shí)進(jìn)行,突出的是"同",比如看足球比賽的時(shí)候吃著零食,兩件事情同時(shí)發(fā)生;異步就是不同時(shí)。但計(jì)算機(jī)中與現(xiàn)實(shí)存在一定差異。
舉個(gè)栗子
天氣冷了,早上剛醒來想喝點(diǎn)熱水暖暖身子,但這每天起早貪黑996,晚上回來太累躺下就睡,沒開水啊,沒法子,只好急急忙忙去燒水。
現(xiàn)在早上太冷了啊,不由得在被窩里面多躺了一會(huì),收拾的時(shí)間緊緊巴巴,不能空等水開,于是我便趁此去洗漱,收拾自己。洗漱完,水開了,喝到暖暖的熱水,舒服??!
舒服完,開啟新的996之日,打工人出發(fā)!
燒水和洗漱是在同時(shí)間進(jìn)行的,這就是計(jì)算機(jī)中的異步。
計(jì)算機(jī)中的同步是連續(xù)性的動(dòng)作,上一步未完成前,下一步會(huì)發(fā)生堵塞,直至上一步完成后,下一步才可以繼續(xù)執(zhí)行。例如:只有等水開,才能喝到暖暖的熱水。
單線程卻可以異步?
JavaScript的確是一門單線程語言,但是瀏覽器UI是多線程的,異步任務(wù)借助瀏覽器的線程和JavaScript的執(zhí)行機(jī)制實(shí)現(xiàn)。例如,setTimeout就借助瀏覽器定時(shí)器觸發(fā)線程的計(jì)時(shí)功能來實(shí)現(xiàn)。
瀏覽器線程
GUI渲染線程繪制頁面,解析HTML、CSS,構(gòu)建DOM樹等 頁面的重繪和重排 與JS引擎互斥(JS引擎阻塞頁面刷新) JS引擎線程js腳本代碼執(zhí)行 負(fù)責(zé)執(zhí)行準(zhǔn)備好的事件,例如定時(shí)器計(jì)時(shí)結(jié)束或異步請(qǐng)求成功且正確返回 與GUI渲染線程互斥 事件觸發(fā)線程 當(dāng)對(duì)應(yīng)的事件滿足觸發(fā)條件,將事件添加到j(luò)s的任務(wù)隊(duì)列末尾 多個(gè)事件加入任務(wù)隊(duì)列需要排隊(duì)等待 定時(shí)器觸發(fā)線程 負(fù)責(zé)執(zhí)行異步的定時(shí)器類事件:setTimeout、setInterval等 瀏覽器定時(shí)計(jì)時(shí)由該線程完成,計(jì)時(shí)完畢后將事件添加至任務(wù)隊(duì)列隊(duì)尾 HTTP請(qǐng)求線程負(fù)責(zé)異步請(qǐng)求 當(dāng)監(jiān)聽到異步請(qǐng)求狀態(tài)變更時(shí),如果存在回調(diào)函數(shù),該線程會(huì)將回調(diào)函數(shù)加入到任務(wù)隊(duì)列隊(duì)尾
同步與異步執(zhí)行順序
JavaScript將任務(wù)分為同步任務(wù)和異步任務(wù),同步任務(wù)進(jìn)入主線中中,異步任務(wù)首先到Event Table進(jìn)行回調(diào)函數(shù)注冊(cè)。當(dāng)異步任務(wù)的觸發(fā)條件滿足,將回調(diào)函數(shù)從 Event Table壓入Event Queue中。主線程里面的同步任務(wù)執(zhí)行完畢,系統(tǒng)會(huì)去 Event Queue中讀取異步的回調(diào)函數(shù)。只要主線程空了,就會(huì)去 Event Queue讀取回調(diào)函數(shù),這個(gè)過程被稱為Event Loop。
舉個(gè)栗子
setTimeout(cb, 1000),當(dāng)1000ms后,就將cb壓入Event Queue。 ajax(請(qǐng)求條件, cb),當(dāng)http請(qǐng)求發(fā)送成功后,cb壓入Event Queue。
EventLoop執(zhí)行流程
Event Loop執(zhí)行的流程如下:
下面一起來看一個(gè)例子,熟悉一下上述流程。
//?下面代碼的打印結(jié)果?
//?同步任務(wù)?打印?first
console.log("first");?????
setTimeout(()?=>?{????????
//?異步任務(wù)?壓入Event?Table?4ms之后cb壓入Event?Queue
??console.log("second");
},0)
//?同步任務(wù)?打印last
console.log("last");?????
//?讀取Event?Queue?打印second
復(fù)制代碼
常見異步任務(wù)
DOM事件AJAX請(qǐng)求定時(shí)器 setTimeout和setlntervalES6的Promise
異步任務(wù)的優(yōu)先級(jí)
下面繼續(xù)來看一個(gè)案例:
setTimeout(()?=>?{
??console.log(1);
},?1000)
new?Promise(function(resolve){
????console.log(2);
????for(var?i?=?0;?i?10000;?i++){
????????i?==?99?&&?resolve();
????}
}).then(function(){
????console.log(3)
});
console.log(4)
復(fù)制代碼
按照上面的學(xué)習(xí):可以很輕松得出案例的打印結(jié)果:2,4,1,3。
Promise定義部分為同步任務(wù),回調(diào)部分為異步任務(wù)
將案例代碼在控制臺(tái)運(yùn)行,最終返回結(jié)果卻有些出人意料:

剛看到如此結(jié)果,我的第一感覺是,setTimeout函數(shù)1s觸發(fā)太慢導(dǎo)致它加入Event Queue的時(shí)間晚于Promise.then
于是我修改了setTimeout的回調(diào)時(shí)間為0(瀏覽器最小觸發(fā)時(shí)間為4ms),但結(jié)果仍為發(fā)生改變。
那么也就意味著,JavaScript的異步任務(wù)是存在優(yōu)先級(jí)的。
宏任務(wù)和微任務(wù)
JavaScript除了廣義上將任務(wù)劃分為同步任務(wù)和異步任務(wù),還對(duì)異步任務(wù)進(jìn)行了更精細(xì)的劃分。異步任務(wù)又進(jìn)一步分為微任務(wù)和宏任務(wù)。

history traversal任務(wù)(h5當(dāng)中的歷史操作)process.nextTick(nodejs中的一個(gè)異步操作)MutationObserver(h5里面增加的,用來監(jiān)聽DOM節(jié)點(diǎn)變化的)
宏任務(wù)和微任務(wù)分別有各自的任務(wù)隊(duì)列Event Queue,即宏任務(wù)隊(duì)列和微任務(wù)隊(duì)列。
Event Loop執(zhí)行過程
了解到宏任務(wù)與微任務(wù)過后,我們來學(xué)習(xí)宏任務(wù)與微任務(wù)的執(zhí)行順序。
代碼開始執(zhí)行,創(chuàng)建一個(gè)全局調(diào)用棧, script作為宏任務(wù)執(zhí)行執(zhí)行過程過同步任務(wù)立即執(zhí)行,異步任務(wù)根據(jù)異步任務(wù)類型分別注冊(cè)到微任務(wù)隊(duì)列和宏任務(wù)隊(duì)列 同步任務(wù)執(zhí)行完畢,查看微任務(wù)隊(duì)列 若存在微任務(wù),將微任務(wù)隊(duì)列全部執(zhí)行(包括執(zhí)行微任務(wù)過程中產(chǎn)生的新微任務(wù)) 若無微任務(wù),查看宏任務(wù)隊(duì)列,執(zhí)行第一個(gè)宏任務(wù),宏任務(wù)執(zhí)行完畢,查看微任務(wù)隊(duì)列,重復(fù)上述操作,直至宏任務(wù)隊(duì)列為空
更新一下Event Loop的執(zhí)行順序圖:

總結(jié)
在上面學(xué)習(xí)的基礎(chǔ)上,重新分析當(dāng)前案例:
setTimeout(()?=>?{
??console.log(1);
},?1000)
new?Promise(function(resolve){
????console.log(2);
????for(var?i?=?0;?i?10000;?i++){
????????i?==?99?&&?resolve();
????}
}).then(function(){
????console.log(3)
});
console.log(4)
復(fù)制代碼
分析過程見下圖:
面試題
文章的最后附贈(zèng)幾道經(jīng)典面試題,可以測(cè)試一下自己對(duì)Event Loop的掌握程度。
題目一
console.log('script?start');
setTimeout(()?=>?{
????console.log('time1');
},?1?*?2000);
Promise.resolve()
.then(function()?{
????console.log('promise1');
}).then(function()?{
????console.log('promise2');
});
async?function?foo()?{
????await?bar()
????console.log('async1?end')
}
foo()
async?function?errorFunc?()?{
????try?{
????????await?Promise.reject('error!!!')
????}?catch(e)?{
????????console.log(e)
????}
????console.log('async1');
????return?Promise.resolve('async1?success')
}
errorFunc().then(res?=>?console.log(res))
function?bar()?{
????console.log('async2?end')?
}
console.log('script?end');
復(fù)制代碼
題目二
setTimeout(()?=>?{
????console.log(1)
},?0)
const?P?=?new?Promise((resolve,?reject)?=>?{
????console.log(2)
????setTimeout(()?=>?{
????????resolve()
????????console.log(3)
????},?0)
})
P.then(()?=>?{
????console.log(4)
})
console.log(5)
復(fù)制代碼
題目三
var?p1?=?new?Promise(function(resolve,?reject){
????resolve("2")
})
setTimeout(function(){
????console.log("1")
},10)
p1.then(function(value){
????console.log(value)
})
setTimeout(function(){
????console.log("3")
},0)
復(fù)制代碼
關(guān)于本文
來源:戰(zhàn)場(chǎng)小包
https://juejin.cn/post/7020328988715270157
