先來個例子
如果能很快知道執行的順序結果,那麼說明你對這塊的內容理解非常深刻。
<div class="parent" data-spm="2.2.2.2">
<div class="child">123</div>
</div>
<script>
var parent = document.getElementsByClassName('parent')[0];
var child = document.getElementsByClassName('child')[0];
parent.addEventListener('click',function (e) {
console.log('parent')
},true);
child.addEventListener('click',function (e) {
console.log('child')
})
new MutationObserver(function() {
console.log('mutate 屬性改變');
}).observe(parent, {
attributes: true
});
child.click();
setTimeout(function () {
console.log('定時器0執行了');
},1000)
setTimeout(function () {
console.log('定時器1執行')
})
setTimeout(function () {
console.log('定時器2執行')
})
new Promise(function (resole) {
setTimeout(function () {
console.log('promise中的定時器執行')
})
console.log('promise 執行')
resole(1);
}).then(function () {
console.log('promise 回調執行');
parent.setAttribute('data-spm','1.1.1.1');
})
console.log('最後一行');
</script>
再來展開我們的話題
前言
熟悉JavaScript的小夥伴肯定知道Event Loop(事件循環),由於JavaScript引擎是單線程的,同時只能處理一個任務,所有當JavaScript碰到某些耗時的任務,比如網絡IO,爲了不阻塞之後的代碼執行,這時候就需要將該任務交由其他的線程去執行,並且監聽一個事件回調,當異步的結果返回時,將該回調推入到異步的隊列中去,而此時假如主線程已經執行完了後續的代碼並且處於空閒狀態,就會去依次執行異步隊列中的代碼。比如下面這樣
setTimeout(function(){
console.log('console start');
});
console.log('console end');
// console end
// console start
但是很多時候,JavaScript代碼中有很多的“異步”操作,要是他們同時出現了,這時候要怎麼判斷他們執行的順序呢。
正文
我們先擬定這麼幾個有關異步的API
1. setTimeout / setInterval
2. promise
3. Dom listener callback
4. MutationObserver
關於這個可能大家不那麼熟悉,不過不影響這篇文章的論述,有興趣可以移步這篇文章
https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver
開始測試
1. 我們先來測試setTimeout
setTimeout(function () {
console.log('定時器1執行')
},0)
setTimeout(function () {
console.log('定時器2執行')
},0)
// 定時器1執行
// 定時器2執行
兩個定時器,延時都爲0,先執行1,再執行2,好理解。
setTimeout(function () {
console.log('定時器1執行')
},100)
setTimeout(function () {
console.log('定時器2執行')
},50)
// 定時器2執行
// 定時器1執行
由於定時器2延時較少,所以更快進入異步隊列,所以事件循環開始時,先執行定時器2的回調。
2. 測試Promise
new Promise(function (resole) {
console.log('promise 1 執行')
resole(1);
}).then(function () {
console.log('promise 1回調執行');
})
new Promise(function (resole) {
console.log('promise 2 執行')
resole(2);
}).then(function () {
console.log('promise 2回調執行');
})
// promise 1 執行
// promise 2 執行
// promise 1回調執行
// promise 2回調執行
可以看到,這裏new Promise中的操作順序執行完後,後續的回調也依次執行,沒有特別的問題
new Promise(function (resolve) {
setTimeout(function(){
resolve(1);
console.log('發佈回調1');
},500)
}).then(function () {
console.log('promise 1回調執行');
})
new Promise(function (resolve) {
setTimeout(function(){
resolve(2);
console.log('發佈回調2');
},100)
}).then(function () {
console.log('promise 2回調執行');
})
// 發佈回調2
// promise 2回調執行
// 發佈回調1
// promise 1回調執行
這個結果可能有一些匪夷所思,先放着,我們等會回頭再來看這個問題。
3. 結合定時器和Promise
setTimeout(function(){
console.log('定時器執行了');
})
new Promise(function(resolve){
console.log('Promise!')
resolve(1);
}).then(function(){
console.log('Promise 回調執行');
});
// Promise!
// Promise 回調執行
// 定時器執行了
不知道小夥伴們會不會有疑問,也許覺得定時器應該在Promise的回調之前執行,這時候就可以扯出一些概念了。
一個猜想
主線程執行完後,假如產生了宏任務和微任務,那麼他們的回調會被分別推到宏任務隊列和微任務隊列中去,等到下一次事件循環來到時,若存在微任務,則會先依次執行完所有的微任務,然後再依次執行所有的宏任務。接着進入下一次事件循環。
我們來對比測試3中的例子看看這個猜想。
由於Promise的回調優先定時器執行了,所以可以猜到Promise的回調屬於微任務,而定時器屬於宏任務。所以雖然定時器先進入異步隊列,但是他是進入到宏任務隊列,而Promise的回調則是進入到微任務隊列,所以事件循環結束後,還是先執行了微任務隊列中的Promise回調
再來看測試2中的代碼,要注意的是,第一次主線程執行完畢後,只產生了倆個宏任務,即來個定時器的回調,因爲還未被resolve,所以微任務還未產生。但是下一次事件循環來臨時,定時器2這個宏任務先執行,要注意的是,在這個宏任務完成後,產生了一個新的微任務,即promise 2回調執行,他被馬上執行了。隨後再執行定時器1這個宏任務,最後執行定時器1產生的微任務。
繼續猜想
假如一次事件循環途中,某個宏任務或者微任務產生了一個微任務,那麼會立即執行完微任務,然後再依次執行其他的宏任務。執行完畢後,再進入到下一次事件循環。
宏任務和微任務怎麼區分
這裏我直接給出結論,大家有興趣可以自行驗證或者尋找資料。
setTimeOut ,setTimeInteral,事件回調 這類屬於宏任務
promise ,mutation這類屬於微任務
驗證剛剛開頭的例子
我們用以上的猜想來看看開頭的例子,一步步分析。
首先給parent和child添加事件監聽,然後我們直接執行 child.click(),注意,這裏不是通過點擊來觸發,所以相當於是直接在主線程中執行這個點擊方法。
1. 此時父節點在捕獲階段先響應,打印出“parent”
2. 然後子節點在冒泡階段,打印出“child”
3. 定時器0被推到宏任務隊列中,等待下一次事件循環,注意延時爲100ms
4. 定時器1被推到宏任務隊列中,等待下一次事件循環,注意延時爲0ms
5. 定時器2被推到宏任務隊列中,等待下一次事件循環,注意延時爲0ms
6. 執行Promise中方法,注意這裏又產生了一個定時器,推到宏任務隊列中,等待下一次事件循環,注意延時爲0ms。然後打印出“ promise 執行”。並且resolve後,產生了一個Promise回調的微任務。並且這個微任務中,又產生了一個mutation的微任務
7. 到主線程最後一行代碼,打印出 “最後一行”
8. 進入下一次事件循環
所以剛剛開始的打印順序是 parent,child,最後一行
新的事件循環開始
9. 在上述的第6步,產生了一個微任務,所以先執行,打印出“ promise 回調執行”,並且產生了新的微任務
10. 馬上執行新的mutation的微任務,打印出 “mutate 屬性改變”。
11. 現在就剩下4個定時器了,按照順序依次執行,所以分別打印出 “定時器1執行”,“定時器2執行”,“promise中的定時器執行”,“定時器0執行”
最終結果
index.html?_ijt=cms0flsm55m1cgqpqrpd72gub4:15 parent
index.html?_ijt=cms0flsm55m1cgqpqrpd72gub4:18 child
index.html?_ijt=cms0flsm55m1cgqpqrpd72gub4:40 promise 執行
index.html?_ijt=cms0flsm55m1cgqpqrpd72gub4:46 最後一行
index.html?_ijt=cms0flsm55m1cgqpqrpd72gub4:43 promise 回調執行
index.html?_ijt=cms0flsm55m1cgqpqrpd72gub4:21 mutate 屬性改變
index.html?_ijt=cms0flsm55m1cgqpqrpd72gub4:31 定時器1執行
index.html?_ijt=cms0flsm55m1cgqpqrpd72gub4:34 定時器2執行
index.html?_ijt=cms0flsm55m1cgqpqrpd72gub4:38 promise中的定時器執行
index.html?_ijt=cms0flsm55m1cgqpqrpd72gub4:27 定時器0執行了
結論
注意到上述步驟6中,promise回調又產生了mutation的微任務,按照之前的猜想,他會被等到下一次事件循環,但是卻在定時器1之前執行了,說明他是在這次事件循環被執行的。所以綜上猜想,我們最後可以給出這樣的結論
第一次主線程執行完後,當前如果有微任務,則先執行完所有的微任務。宏任務就推到宏任務隊列中,等待下一次事件循環。然後進入下一次事件循環
然後,每執行完一個宏任務後,檢測當前是否有微任務產生,有就立即執行所有微任務。有宏任務產生,則推到宏任務中,等待下一次事件循環。
接着依次重複執行完本次事件循環中的所有宏任務。然後進入下一次事件循環
上面的結論,略顯繁瑣,其實假如我們把第一次主線程的執行,即script下的所有代碼,看成是第一次宏任務,那麼核心結論可以變成這樣
假如主線程執行棧不爲空,那麼執行完所有宏任務後,再執行所有微任務
否則,執行完一個宏任務後,會立即執行所有微任務
以上測試均在Chrome中進行。
後記
其實這些東西主要是要在腦子中形成一個印象,用來加深理解 事件循環 。而且,在Node的世界裏,比如還會有其他的微任務,比如Process.nextTick。弄懂這些,對JavaScript的執行能有更好的把握。