對於普通的事件的執行過程:
js會將同步和異步任務分別放入不同的執行"場所",同步的進入主線程(先執行,同步任務實質上是一個宏任務),異步任務進入Event Table並註冊函數。當指定的事情完成時,Event Table會將這個函數移入Event Queue。(指定的事情比如setTimeout的定義的時間完成時)主線程內的任務執行完畢爲空,會去Event Queue讀取對應的函數,進入主線程執行。上述過程會不斷重複,也就是常說的Event Loop(事件循環)。如下圖:
但但是js異步有一個機制,就是遇到宏任務,先執行宏任務,將宏任務放入eventqueue,然後再執行微任務,將微任務放入eventqueue。最騷的是,這兩個queue不是一個queue。當你往外拿的時候先從微任務裏拿這個回調函數,然後再從宏任務的queue上拿宏任務的回調函數。異步事件執行的最快時間是4ms。
宏任務和微任務的分類
macro task(宏任務): 同步代碼(整塊script代碼)、setImmediate、MessageChannel、setTimeout/setInterval
micro task(微任務): Promise.then(不是promise,promise會立即執行)、MutationObserver,還有process.nextTick等。
下面以一道關於promise, setTimeout和promise.then的題目爲例:
setTimeout(function() {
console.log('setTimeout');
})
new Promise(function(resolve) {
console.log('promise');
}).then(function() {
console.log('then');
})
console.log('console');
- 這段代碼作爲宏任務,進入主線程。
- 先遇到setTimeout,然後將其回調函數註冊後分發到宏任務Event Queue。(註冊過程與上同,下文不再描述)
- 接下來遇到了Promise,new Promise立即執行,then函數分發到微任務Event Queue。
- 遇到console.log(),立即執行。
- 好啦,整體代碼script作爲第一個宏任務執行結束,看看有哪些微任務?我們發現了then在微任務Event Queue裏面,執行。
- ok,第一輪事件循環結束了,我們開始第二輪循環,當然要從宏任務Event Queue開始。我們發現了宏任務Event Queue中setTimeout對應的回調函數,立即執行。
- 結束。
事件循環、宏任務,微任務的關係如圖
事件循環的順序,決定js代碼的執行順序。進入整體代碼(宏任務)後,開始第一次循環。接着執行所有的微任務。然後再次從宏任務開始,找到其中一個任務隊列執行完畢,再執行所有的微任務。
我們來分析一段較複雜的代碼,看看你是否真的掌握了js的執行機制:
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
第一輪事件循環流程分析如下:
- 整體script作爲第一個宏任務進入主線程,遇到
console.log
,輸出1。 - 遇到
setTimeout
,其回調函數被分發到宏任務Event Queue中。我們暫且記爲setTimeout1
。 - 遇到
process.nextTick()
,其回調函數被分發到微任務Event Queue中。我們記爲process1
。 - 遇到
Promise
,new Promise
直接執行,輸出7。then
被分發到微任務Event Queue中。我們記爲then1
。 - 又遇到了
setTimeout
,其回調函數被分發到宏任務Event Queue中,我們記爲setTimeout2
。
宏任務Event Queue | 微任務Event Queue |
---|---|
setTimeout1 | process1 |
setTimeout2 | then1 |
-
上表是第一輪事件循環宏任務結束時各Event Queue的情況,此時已經輸出了1和7。
-
我們發現了
process1
和then1
兩個微任務。 - 執行
process1
,輸出6。 - 執行
then1
,輸出8。
好了,第一輪事件循環正式結束,這一輪的結果是輸出1,7,6,8。那麼第二輪時間循環從setTimeout1
宏任務開始:
- 首先輸出2。接下來遇到了
process.nextTick()
,同樣將其分發到微任務Event Queue中,記爲process2
。new Promise
立即執行輸出4,then
也分發到微任務Event Queue中,記爲then2
。
宏任務Event Queue | 微任務Event Queue |
---|---|
setTimeout2 | process2 |
then2 |
- 第二輪事件循環宏任務結束,我們發現有
process2
和then2
兩個微任務可以執行。 - 輸出3。
- 輸出5。
- 第二輪事件循環結束,第二輪輸出2,4,3,5。
- 第三輪事件循環開始,此時只剩setTimeout2了,執行。
- 直接輸出9。
- 將
process.nextTick()
分發到微任務Event Queue中。記爲process3
。 - 直接執行
new Promise
,輸出11。 - 將
then
分發到微任務Event Queue中,記爲then3
。
宏任務Event Queue | 微任務Event Queue |
---|---|
process3 | |
then3 |
- 第三輪事件循環宏任務執行結束,執行兩個微任務
process3
和then3
。 - 輸出10。
- 輸出12。
- 第三輪事件循環結束,第三輪輸出9,11,10,12。
整段代碼,共進行了三次事件循環,完整的輸出爲1,7,6,8,2,4,3,5,9,11,10,12。
(請注意,node環境下的事件監聽依賴libuv與前端環境不完全相同,輸出順序可能會有誤差)
6.寫在最後
(1)js的異步
我們從最開頭就說javascript是一門單線程語言,不管是什麼新框架新語法糖實現的所謂異步,其實都是用同步的方法去模擬的,牢牢把握住單線程這點非常重要。
(2)事件循環Event Loop
事件循環是js實現異步的一種方法,也是js的執行機制。
(3)javascript的執行和運行
執行和運行有很大的區別,javascript在不同的環境下,比如node,瀏覽器,Ringo等等,執行方式是不同的。而運行大多指javascript解析引擎,是統一的。
(4)setImmediate
微任務和宏任務還有很多種類,比如setImmediate
等等,執行都是有共同點的,有興趣的同學可以自行了解。
(5)最後的最後
- javascript是一門單線程語言
- Event Loop是javascript的執行機制
牢牢把握兩個基本點,以認真學習javascript爲中心,早日實現成爲前端高手的偉大夢想!