前言
還記得那些年面試官問你的定時器的原理嗎?還有呢?Promise的原理呢琼讽?原理必峰、原理、原理钻蹬,問的我們懷疑人生吼蚁。
為了下次不再懵逼,今天问欠,我們來了解一下Event Loop的概念肝匆。我們的初衷是真正的了解和掌握它,了解整體JavaScript的運行機制溅潜。至少术唬,我們在看完文章的時候,不會讓我們在懷疑人生了滚澜。如果你喜歡我的文章粗仓,歡迎評論,歡迎Star~
正文
聊這一個話題设捐,我們必須從JavaScript本身聊起借浊。
為什么瀏覽器選擇了我
眾所周知,JavaScript是一門單線程語言萝招。為什么會在瀏覽器端開發(fā)一門這樣子的單線程原因呢蚂斤?原因就是——簡單。在瀏覽器端槐沼,復雜的UI環(huán)境會限制多線程語言的開發(fā)曙蒸。例如,一個線程在操作一個DOM元素時岗钩,另一個線程需要去刪除DOM元素纽窟,這個之間就需要進行狀態(tài)的同步,何況前端可能不止操作這么一個DOM元素兼吓。所以臂港,為了避免在開發(fā)過程中,去進行復雜的同步视搏,選用單線程語言進行開發(fā)是最好的解決方案审孽。
但是浑娜,單線程語言也會有問題——同步阻塞佑力。舉個例子,cpu在執(zhí)行程序的過程中筋遭,執(zhí)行到IO操作,它會發(fā)送IO請求,之后等待數(shù)據(jù)返回养距,待數(shù)據(jù)返回之后才會繼續(xù)執(zhí)行后面的任務,如圖束析。
這種問題在瀏覽器中恶守,或許是致命的利花。因此,JavaScript在執(zhí)行過程中,將任務分為同步任務和異步任務。
同步和異步的我
首先娩贷,我們來了解一下主線程的執(zhí)行棧(call stack)。一個線程對應著一個執(zhí)行棧储笑,所有同步任務都會被放入到執(zhí)行棧中執(zhí)行秕磷,例如贩汉,我們有下面這樣子的一個程序:
function fun1(){
return 'hello hip-hop';
}
function fun2(){
return fun1();
}
function fun3(){
console.log(fun2());
}
fun3(); //'hello hip-hop'
調用棧執(zhí)行過程:
或者我們使用另一種方法來論證一下:我們將fun1改成throw new Error('hello hip-hop')
function fun1(){
throw new Error('hello hip-hop');
}
function fun2(){
return fun1();
}
function fun3(){
console.log(fun2());
}
fun3();
每次調用過程中,會有函數(shù)的出棧和入棧锚赤。這就是同步任務執(zhí)行的過程匹舞,一個任務執(zhí)行完成之后,執(zhí)行下一個任務线脚。那么赐稽,我們在來看一下另外一個例子:
console.log('first');
setTimeout(() => {
console.log('second');
}, 500);
console.log('three');
我們依舊來看一下這個程序的調用棧的執(zhí)行順序:
從圖中,我們可以看到setTimeout執(zhí)行完成之后浑侥,已經(jīng)出棧了姊舵,但是后來的console.log('second')又是如何入棧的呢?
其實寓落,在主線程之外括丁,還有一個任務隊列。在任務隊列中伶选,會存放著異步任務史飞,只有當指定事件發(fā)生之后,異步任務才會被放到主線程中執(zhí)行仰税。
其實构资,任務隊列中是一個事件隊列,那setTimeout舉例來說陨簇,當主線程執(zhí)行setTimeout的時候吐绵,會創(chuàng)建一個定時器,一旦定時器的時間達到了河绽,就會將其內部的回調函數(shù)己单,放入任務隊列中。然后葵姥,主線程在執(zhí)行的最后去循環(huán)任務隊列荷鼠。而回調函數(shù),指的是主線程掛起的代碼榔幸,異步任務必須有回調函數(shù)才能執(zhí)行接下來的操作允乐。如圖:
事件循環(huán)
其實矮嫉,上圖中,我們可以看到一個循環(huán)牍疏,如圖:
這是一個死循環(huán)蠢笋,無論哪種情況都是閉環(huán)。其實鳞陨,這就是事件循環(huán)昨寞。事件循環(huán)會不斷地去檢測任務隊列中是否還有已觸發(fā)時間的任務,如果有的話厦滤,就放入主線程中執(zhí)行援岩。但是事件循環(huán)一般會在主線程中任務執(zhí)行完成之后執(zhí)行。我們可以來看一下另一幅圖片:
這幅圖片中掏导,我們可以看到完整的執(zhí)行流程享怀,其中涉及到的異步事件有DOM事件、ajax請求和setTimeout趟咆。所以添瓷,整體的執(zhí)行流程是這樣子的:
(1)、所有同步任務會在主線程的調用棧中執(zhí)行值纱。
(2)鳞贷、在主線程之外,還有一個任務隊列虐唠,一旦指定事件發(fā)生之后搀愧,異步任務就會被放入任務隊列中
(3)、當主線程執(zhí)行完調用棧中的同步任務時疆偿,會遍歷任務隊列妈橄,將任務隊列中的任務放入主線程中執(zhí)行。而之后事件循環(huán)一直會去遍歷任務隊列翁脆,一旦有任務放入就會放入主線程中執(zhí)行眷蚓。
這樣,我們就已經(jīng)初步了解了同步和異步之間的實現(xiàn)反番,以及瀏覽器中的事件循環(huán)機制沙热。
任務隊列的不同
自從ES6標準出來之后,Promise就被開發(fā)者關注到了罢缸。Promise是個很有意思的家伙篙贸,詳細講解的話,篇幅會太長枫疆。我們只關注它異步方面的任務隊列爵川。
首先,我們來看一段程序:
setTimeout(() => {
console.log(1);
}, 0);
Promise.resolve().then(() => {
console.log(2);
}).then(() => {
console.log(3);
});
console.log(4); // 4 2 3 1
你會對它的輸出感到疑惑嗎息楔?相信經(jīng)歷過面試的你一定會寝贡?還是兩個字——懵逼
其實扒披,這個執(zhí)行順序和任務隊列的種類有關系。我們一般一直稱呼地任務隊列(task queue)圃泡,其實指的是Macrotasks碟案。而Promise執(zhí)行后會被放到Microtasks中。
Macrotasks => 一般會將dom事件颇蜡、ajax事件和setTimeout放入到這個隊列中价说。
Microtasks => 一般會將Promise、process.nextTicks风秤、MutationObserver放入這個隊列中鳖目。
在執(zhí)行事件循環(huán)時,主線程會首先遍歷Microtasks缤弦,然后將隊列中的異步任務抽取出來執(zhí)行疑苔,直至抽空整個隊列,才會去執(zhí)行Macrotasks的隊列中的異步任務甸鸟。
所以,上面函數(shù)的調用棧過程如下:
總結
js的事件循環(huán)部分兵迅,內容應該算是全部闡述完全了抢韭。希望對看的你,會有收獲恍箭。
本文來自我的github刻恭,原文地址
如果你對我寫的有疑問是越,可以評論姑食,如我寫的有錯誤猿棉,歡迎指正满葛。你喜歡我的博客蜂林,請給我關注Star~呦