什么是事件循環機制
相信大家看過很多類似下面這樣的代碼:
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
事件是宏任務
還是微任務
年后有時間在復盤總結這兩個問題吧。
最后提前祝大家在新的一年好運哦~
近期文章
寫在最后
如果這篇文章有幫助到你,❤️關注+點贊❤️鼓勵一下作者
文章公眾號
首發,關注 不知名寶藏程序媛
第一時間獲取最新的文章
筆芯❤️~