什么是事件循環機制
相信大家看過很多類似下面這樣的代碼:
function printNumber(){
console.log('printNumber');
}
setTimeout(function(){
console.log('setTimeout 1000')
}, 1000);
setTimeout(function(){
console.log('setTimeout 0')
});
printNumber();
new Promise((resolve, reject) => {
console.log('new promise');
}).then(function(){
console.log('promise resolve');
})
然后讓我們說出這段代碼的輸出結果,那這段代碼的輸出結果其實就是由事件循環機制決定的。
我們都知道JS引擎線程是專門用來解析JavaScript腳本的,所有的JavaScript代碼都由這一個線程來解析。然而這個JS引擎是單線程的,也就意味着JavaScript程序在執行時,前面的必須處理好,后面的才會執行。
但是JavaScript中除了一些順序執行的邏輯代碼,還有很多異步任務,比如Ajax請求、定時器等。如果JS引擎在單線程解析JavaScript時遇到了一個Ajax請求,那就必須等Ajax請求返回結果才能繼續執行后續的代碼,很顯然這樣的行為是非常低效的。
那為了解決這樣的問題,事件循環機制這樣的技術就顯得尤為重要:
"JS引擎"在順序執行"JavaScript"代碼時,如果遇到"同步代碼"立即執行;
如果遇到一些"異步任務"就會將這個"異步任務"交給對應的模塊處理,然后繼續執行后續代碼;
當一個"異步任務"到達觸發條件時就將該"異步任務"的回調放入"任務隊列"中;
當"JS引擎"空閑以后,就會從"任務隊列"讀取和執行異步任務;
補充內容:
1.
JS引擎線程也被稱為執行JS代碼的主線程,后續如果出現主線程這樣的描述,指的就是JS引擎線程。
2.任務隊列屬於數據結構中的隊列,特性是先進先出。
3. 有關JavaScript中同步任務和異步任務的分類下面一節會介紹。
4. 本文只討論瀏覽器環境下的事件循環機制,后續的描述和代碼演示均基於瀏覽器環境(Node中的事件循環機制不做分析)。
JavaScript任務的分類
前面我們簡單介紹過事件循環機制執行JS代碼的順序,那首先我們需要知道在JavaScript那些代碼是同步任務,那些是異步任務。
接下來我們對JavaScript中的任務做一個分類:

這個分類很重要哦 不同的類型的任務執行順序不同~
任務的執行順序
接着事件循環機制中JS引擎對這些任務的執行順序描述如下:
-
步驟一: 從
<script>代碼開始,到</script>代碼結束,按順序執行所有的代碼。 -
步驟二: 在
步驟一順序執行代碼的過程中,如果遇到同步任務,立即執行,然后繼續執行后續代碼;如果遇到異步任務,將異步任務交給對應的模塊處理(事件交給事件處理線程,ajax交給異步HTTP請求線程),當異步任務到達觸發條件以后將異步任務的回調函數推入任務隊列(宏任務推入宏任務隊列,微任務推入微任務隊列)。 -
步驟三:
步驟一結束后,說明同步代碼執行完畢。此時讀取並執行微任務隊列中保存的所有的微任務。 -
步驟四:
步驟三完成后讀取並執行宏任務隊列中的宏任務,每執行完一個宏任務就去查看微任務隊列中是否有新增的微任務,如果存在則重復步驟三;如果不存在,繼續執行下一個宏任務,直到。

一定要看的補充說明 !!!
1.步驟四中描述的
新增的微任務和步驟三中描述的微任務是一樣的,因為異步任務只有滿足條件以后才會被推入任務隊列,步驟三在執行時,不一定所有的微任務都到達觸發條件而被推入任務隊列;2.所謂的
到達觸發條件指的是下面這幾種情況:
① 定時器:定時器設置的時間到達,才會將定時器的回調函數推入任務隊列中
② DOM事件:DOM綁定的事件被觸發以后,才會將事件的回調函數推入任務隊列中
③ 異步請求:異步請求返回結果以后,才會將異步請求的回調函數推入任務隊列中
④ 異步任務之間的互相嵌套:比如宏任務A嵌套微任務X,當宏任務A對應的回調函數代碼沒有被執行到的時候,很顯然根本不存在微任務X;只有宏任務A對應的回調函數代碼被執行以后,JS引擎才會解析到微任務X,此時依然是將該微任務X交給對應的線程去處理,當微任務X滿足前面描述的①、②、③的條件,才會將微任務X對應的回調推入任務隊列,等待JS引擎去執行。3.所有的
異步任務都在JS引擎遇到</script>以后才會開始執行。4.
宏任務對應的英文描述為task,微任務對應的英文描述為micro task;宏任務隊列描述為task quene,微任務隊列描述為micro task quene。不過很多文章也會將宏任務描述為macro task,這個沒多大關系。只是有些文章會將micro task描述為微任務隊列,就有些誤導人了,本文為了避免描述上產生的問題,均用中文文字描述。
實踐一波吧
到此事件循環機制的核心內容就講完了,核心內容主要就兩點:JavaScript任務分類和任務執行順序。只要牢牢掌握這兩點,就能解決大部分問題。
那接下來我們就來實踐一下。
示例一
console.log('script start');
function printNumber(){
console.log('同步任務執行:printNumber');
}
setTimeout(function(){
console.log('宏任務執行:setTimeout 1000ms')
}, 1000);
printNumber();
new Promise((resolve, reject) => {
console.log('同步任務執行:new promise');
resolve();
}).then(function(){
console.log('微任務執行:promise resolve');
})
console.log('script end');
這段代碼是文章開頭貼出來的代碼,相對來說比較簡單,接下來就分析一下這段代碼的執行順序以及輸出結果。
-
1.首先
js引擎從上到下開始執行代碼 -
2.遇到
console.log直接打印:script start -
3.遇到
函數聲明 -
4.遇到宏任務
setTimeout,交給定時器線程去處理(定時器線程會在1000ms后將setTimeout的回調函數:function(){ console.log('宏任務執行:setTimeout 1000ms') }推入宏任務隊列,等待JS引擎去執行),之后JS引擎繼續執行后續代碼 -
5.遇到函數調用:
printNumber,立即執行並打印:同步任務執行:printNumber -
6.遇到
new Promise,new Promise構造傳入的內容立即執行,所以打印:console.log('同步任務執行:new promise'); -
7.遇到
resolve執行promise.then,promise.then屬於微任務,因此將promise.then的回調函數:function(){ console.log('微任務執行:promise resolve'); }推入微任務隊列 -
8.再次遇到
console.log直接打印script end -
9.
步驟8完成,即說明同步任務執行完畢。此時就開始讀取並執行微任務隊列中所有的微任務。 在本例中就是執行步驟7中的promise.then,即打印:微任務執行:promise resolve。 -
10.本例中只有一個
微任務,因此步驟9完成以后開始執行宏任務,也就是步驟4中setTimeout的回調,即打印:宏任務執行:setTimeout 1000ms
注意:
setTimeout定時器設置的時間實際是推入任務隊列的時間
經過以上的分析,得出來的打印順序如下:
script start
同步任務執行:printNumber
同步任務執行:new promise
script end
微任務執行:promise resolve
宏任務執行:setTimeout 1000ms
最后在瀏覽器中驗證一下:

示例二
接下來我們來看看下面這個稍微復雜一些的案例:
console.log('script start');
setTimeout(function(){
console.log('宏任務執行:setTimeout1 2000ms')
}, 2000);
setTimeout(function(){
console.log('宏任務執行:setTimeout2 0ms')
}, 0);
new Promise((resolve, reject) => {
console.log('同步代碼執行: new Promise');
setTimeout(function(){
console.log('宏任務執行:setTimeout3 1000ms')
resolve();
}, 1000);
}).then(function(){
console.log('微任務執行:promise resolve')
});
console.log('script end');
分析執行過程:
-
1.
js引擎從上到下開始執行代碼 -
2.遇到
console.log直接打印:script start -
3.遇到
宏任務setTimeout,交給定時器線程處理(定時器線程會在2000ms后將setTimeout的回調函數:function(){ console.log('宏任務執行:setTimeout1 2000s') }推入宏任務隊列,等待JS引擎去執行),JS引擎繼續執行后續代碼 -
4.再次遇到
宏任務setTimeout,交給定時器線程處理(定時器線程會在0ms后將setTimeout的回調函數:function(){ console.log('宏任務執行:setTimeout2 0s') }推入宏任務隊列,等待JS引擎去執行),JS引擎繼續執行后續代碼 -
5.遇到
new Promise,new Promise構造傳入的內容立即執行,所以打印:console.log('同步任務執行:new promise');; -
6.接着發現
new Promise的構造函數存在一個宏任務setTimeout,所以依然是交給定時器線程處理(定時器線程會在1000ms后將改setTimeout的回調函數:function(){ console.log('宏任務執行:setTimeout3 1000ms') }推入宏任務隊列,等待JS引擎去執行),JS引擎繼續執行后續代碼 -
7.遇到
console.log直接打印:script end -
8.
步驟7完成,即說明同步任務執行完畢。在這個過程中,沒有產生微任務,所以微任務隊列為空;同時在這個過程中產生了三個宏任務:setTimeout,按照定時器設置的時間,這三個宏任務推入宏任務隊列的順序為:setTimeout2 0ms、setTimeout3 1000ms、setTimeout1 2000ms,所以后續執行宏任務時先推入隊列的任務先執行。(最先推入任務隊列的稱為隊首的任務,任務執行完成后,就會從隊首中移除,下一個任務就會稱為隊首任務) -
9.根據
步驟8的分析,執行完同步代碼以后,本應該先執行微任務隊列中的所有的微任務,但是因為並沒有微任務存在,所以開始執行宏任務隊列中隊首的任務,即setTimeout2 0ms,所以會打印:宏任務執行:setTimeout2 0ms -
10.
步驟9結束以后,也就是執行完一個宏任務了;接下依然是執行微任務隊列中的所有微任務,但是此時依然因為沒有微任務存在,所以執行宏任務隊列中的隊首的那個任務,即setTimeout3 1000ms,所以會打印:宏任務執行:setTimeout2 0ms; 接着發現定時器setTimeout的回調函數中調用了resolve,因此產生了一個微任務:promise.then,該微任務會被推入微任務隊列。 -
11.
步驟10結束以后,也是執行完一個宏任務了;接下還是執行微任務隊列中的所有微任務,此時微任務隊列中有一個微任務,是步驟9在執行的過程中產生的(這就是我們在前面說的任務之間的嵌套,只有外層任務的回調被執行后,內層的任務才會存在),所以執行該微任務,打印:微任務執行:promise resolve -
步驟10完成后,即執行完一個微任務;接着繼續執行宏任務隊列中隊首的那個任務,即打印:setTimeout1 2000s
-
13.所有的
微任務、宏任務執行完畢,代碼結束
最終的打印順序:
script start
同步代碼執行: new Promise
script end
宏任務執行:setTimeout2 0ms
宏任務執行:setTimeout3 1000ms
微任務執行:promise resolve
宏任務執行:setTimeout1 2000ms
瀏覽器在驗證一下:

setImmediate和setTimeout 0
關於setImmediate的作用 MDN Web Docs 是這樣介紹的:

從上面的描述我們可以獲取到兩個有用信息:
- 1.該方法是非標准的,目前只有最新版的
IE和Nodejs 0.10+支持 - 2.該方法提供的回調函數會在瀏覽器完成后面的其他語句后立即執行
關於第一點非常好理解,我自己也做過嘗試,確實只有IE10以及更高的版本才能使用;
而第二點說的有點含糊,我個人理解為setImmediate的回調應該是在JS引擎執行完所有的同步代碼以后立即執行的。
那不管如何理解,我們在IE瀏覽器中試試應該能得出更准確的結論。
以下所有的示例均在
IE11中進行測試
示例一
首先是一個最簡單的示例:
console.log('script start');
setImmediate(function(){
console.log('宏任務執行:setImmediate');
})
console.log('script end');
這段代碼的輸出順序如下:

從這個示例的結果可以看到setImmediate的回調函數確實是在同步代碼執行完成后才執行的。這個結果能說明前面的理解是正確的嗎?
先不要着急,我們在來看一個示例。
示例二
console.log('script start');
setImmediate(function(){
console.log('宏任務執行:setImmediate');
})
setTimeout(function(){
console.log('宏任務執行:setTimeout 0');
},0)
console.log('script end');
在這個示例中,我們寫了一個setTimeout定時器,並且將時間設置為0。根據代碼書寫順序,在將setImmediate推入宏任務隊列以后,緊接着setTimeout的回調也會被推入宏任務隊列,所以最終應該輸出:
script start
script end
宏任務執行:setImmediate
宏任務執行:setTimeout 0
但是瀏覽器的輸出結果並不是這樣的:

示例三
console.log('script start');
setTimeout(function(){
setImmediate(function(){
console.log('宏任務執行:setImmediate');
})
setTimeout(function(){
console.log('宏任務執行:setTimeout 0');
},0)
}, 0)
console.log('script end');
在這個例子中,我們將setImmediate和setTimeout 0是嵌套在異步任務setTimeout的里面,並且外層的setTimeout設置的時間是0ms。
然而令人困惑的是這段代碼在IE瀏覽器中的輸出結果是不確定的:

以上是多次刷新頁面的輸出結果
經過以上三個示例,關於setImmediate和setTimeout 0兩者的執行時機貌似得不出什么合適的結論,所以這個問題先不做總結,后續在研究吧~
setTimeout 0 和setTimeout 1
在學習這個的時候看到一個特別有意思的代碼:
setTimeout(function(){
console.log('宏任務執行:setTimeout 1ms');
}, 1)
setTimeout(function(){
console.log('宏任務執行:setTimeout 0ms');
}, 0)
如果按照事件循環機制的說法,理論上以上的代碼輸出結果為:
script start
script end
宏任務執行:setTimeout 0
宏任務執行:setTimeout 1
這段代碼在Firefox和IE中確實是前面我們推測出來的結果:

但是在Chrome中卻是下面這樣的結果:

Chrome瀏覽器的輸出結果貌似有點違背前面總結的事件循環機制,但是實際上並沒有,因為我們有一句非常重要的話:當異步任務到達觸發條件以后將異步任務的回調函數推入任務隊列。
所以對於setTimeout 1是在1ms后將回調函數推入任務隊列,setTimeout 0則是立即將回調函數推入任務隊列,然而setTimeout 1在setTimeout 0的前面,執行完setTimeout 1以后,當執行setTimeout 0的時候1ms的時間已經過去了,那這個時候setTimeout 1的回調函數就比setTimeout 0的回調函數先壓入任務隊列,所以就會出現Chrome中的打印結果。
Ajax和Dom事件的疑惑
前面我們在對JS中的任務分類時,對Ajax和Dom事件並沒有進行歸類,一個原因是發現很多文章並沒有對這兩個任務進行分類,也有很多文章對這兩個任務的分類都不一致;另外一個原因就是我自己也沒有找到一些合適的例子去證實。
不過關於事件循環機制 HTML Standard有關於 Event Loop 的介紹,不過介於全篇是純英文的,簡單看過之后只get到了下面的這些有效信息:

在經過翻譯和解讀以后,得出來下面這些信息。
每一個任務都有相關的任務源
function fn(){ }
setTimeout(fn, 1000)
在上面的例子中fn就稱為是setTimeout的回調函數,setTimeout就稱為該回調函數的任務源。
推入任務隊列的是對應的回調函數,執行回調函數的時候可以稱為在執行任務。所以在該示例中就可以說任務fn對應的任務源就是setTimeout。
瀏覽器會根據任務源去分類所有的任務
這個就是前面我們第二節中總結的JavaScript中任務的分類。
瀏覽器有一個用於鼠標和按鍵事件的任務隊列
關於這個說法的完整翻譯為:瀏覽器可以有一個用於鼠標和按鍵事件的任務隊列(與用戶交互任務源關聯),另一個與所有其他任務源關聯。然后,使用在事件循環處理模型的初始步驟中授予的自由度,它可以使鍵盤和鼠標事件優先於其他任務四分之三的時間,從而保持界面的響應性,但不會耗盡其他任務隊列。
看完這段話,我會理解DOM事件是不是區別於前面我們說的宏任務、微任務?
總而言之呢,關於Ajax和Dom事件到底是屬於微任務還是宏任務?以及它們兩個和其他異步任務共同存在時的執行順序,我自己還是存疑的,所以就不給出什么結論了,以免誤導大家。當然如果大家有明確的結論或者示例,歡迎提出來~
總結
到此本篇文章就結束了,有關瀏覽器中的事件循環機制就我們總結的兩個核心點:JavaScript任務分類和任務執行順序。
只要牢牢掌握這兩點,就能解決大部分問題。
然而本篇文章還遺留了兩個問題:
- 瀏覽器中
setTimeout 0和setImmediate執行順序 ajax和dom事件是宏任務還是微任務
年后有時間在復盤總結這兩個問題吧。
最后提前祝大家在新的一年好運哦~
近期文章
寫在最后
如果這篇文章有幫助到你,❤️關注+點贊❤️鼓勵一下作者
文章公眾號首發,關注 不知名寶藏程序媛 第一時間獲取最新的文章
筆芯❤️~

