在之前的一篇文章中簡單理了下JS的運行機制,順著這條線深入就又遇到了幾個概念,什么是事件回圈,什么又是宏任務、微任務呢,今天用這篇文章梳理一下,
以下是我自己的理解,如有錯誤,還望不吝賜教,
事件回圈與訊息佇列
首先大家都知道JS是一門單執行緒的語言,所有的任務都是在一個執行緒上完成的,而我們知道,有一些像I/O,網路請求等等的操作可能會特別耗時,如果程式使用"同步模式"等到任務回傳再繼續執行,就會使得整個任務的執行特別緩慢,運行程序大部分事件都在等待耗時操作的完成,效率特別低,
為了解決這個問題,于是就有了事件回圈(Event Loop)這樣的概念,簡單來說就是在程式本身運行的主執行緒會形成一個"執行堆疊",除此之外,設立一個"任務佇列",每當有異步任務完成之后,就會在"任務佇列"中放置一個事件,當"執行堆疊"所有的任務都完成之后,會去"任務佇列"中看有沒有事件,有的話就放到"執行堆疊"中執行,
這個程序會不斷重復,這種機制就被稱為事件回圈(Event Loop)機制,
宏任務/微任務
宏任務可以被理解為每次"執行堆疊"中所執行的代碼,而瀏覽器會在每次宏任務執行結束后,在下一個宏任務執行開始前,對頁面進行渲染,而宏任務包括:
- script(整體代碼)
- setTimeout
- setInterval
- I/O
- UI互動事件
- postMessage
- MessageChannel
- setImmediate
- UI rendering
微任務,可以理解是在當前"執行堆疊"中的任務執行結束后立即執行的任務,而且早于頁面渲染和取任務佇列中的任務,宏任務包括:
- Promise.then
- Object.observe
- MutaionObserver
- process.nextTick
他們的運行機制是這樣的:
- 執行一個宏任務(堆疊中沒有就從事件佇列中獲取)
- 執行程序中如果遇到微任務,就將它添加到微任務的任務佇列中
- 宏任務執行完畢后,立即執行當前微任務佇列中的所有微任務(依次執行)
- 當前宏任務執行完畢,開始檢查渲染,然后GUI執行緒接管渲染
- 渲染完畢后,JS執行緒繼續接管,開始下一個宏任務(從事件佇列中獲取)

在了解了宏任務和微任務之后,整個Event Loop的流程圖就可以用下面的流程圖來概括:

例子
如無特殊說明,我們用setTimeout來模擬異步任務,用Promise來模擬微任務,
主執行緒上有宏任務和微任務
console.log('task start');
setTimeout(()=>{
console.log('setTimeout')
},0)
new Promise((resolve, reject)=>{
console.log('new Promise')
resolve()
}).then(()=>{
console.log('Promise.then')
})
console.log('task end');
//----------------------執行結果----------------------
// task start
// new Promise
// task end
// Promise.then
// setTimeout
這個例子比較簡單,就是在主任務上加了一個宏任務(setTimeout),加了一個微任務(Promise.then),看執行的順序,列印出了主任務的task start、new Promise、task end,主任務完成,接下來執行了微任務的Promise.then,到此第一輪事件回圈結束,去任務佇列里取出了setTimeout并執行,
在微任務中添加宏任務和微任務
跟上個例子相比,我們在Promise.then里加上一個setTimeout和一個Promise.then,
console.log('task start');
setTimeout(()=>{
console.log('setTimeout1')
},0)
new Promise((resolve, reject)=>{
console.log('new Promise1')
resolve()
}).then(()=>{
console.log('Promise.then1')
setTimeout(()=>{
console.log('setTimeout2')
},0)
new Promise((resolve, reject)=>{
console.log('new Promise2')
resolve()
}).then(()=>{
console.log('Promise.then2')
})
})
console.log('task end');
//----------------------執行結果----------------------
// task start
// new Promise1
// task end
// Promise.then1
// new Promise2
// Promise.then2
// setTimeout1
// setTimeout2
猜對了么,正常的主任務沒有變化,只是在執行第一次微任務的時候,發現了一個宏任務,于是被加進了任務對了,遇到了一個微任務,放到了微任務佇列,執行完之后又掃了一遍微任務佇列,發現有微任務,于是接著執行完微任務,到這,第一遍事件回圈才結束,從任務佇列里拿出了兩次setTimeout執行了,
在異步宏任務中添加宏任務和微任務
其他無異,把剛才添加到Promise.then中的內容添加到setTimeout中,
console.log('task start')
setTimeout(()=>{
console.log('setTimeout1')
setTimeout(()=>{
console.log('setTimeout2')
},0)
new Promise((resolve, reject)=>{
console.log('new Promise2')
resolve()
}).then(()=>{
console.log('Promise.then2')
})
},0)
new Promise((resolve, reject)=>{
console.log('new Promise1')
resolve()
}).then(()=>{
console.log('Promise.then1')
})
console.log('task end')
//----------------------執行結果----------------------
// task start
// new Promise1
// task end
// Promise.then1
// setTimeout1
// new Promise2
// Promise.then2
// setTimeout2
第一遍主任務執行大家都很明白了,到Promise.then1結束,然后取任務佇列中的setTimeout,執行程序中又發現了一個setTimeout,放到任務佇列中,并且發現一個Promise.then2,把這個微任務執行完之后,第二遍事件回圈才結束,然后開始第三遍,列印出了setTimeout2,
加入事件冒泡
事件回圈遇到事件冒泡會發生什么?
<div >
<div ></div>
</div>
var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');
function onClick() {
console.log('click');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('new Promise');
});
}
inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);
點擊inner,結果:
click //inner的click
promise //inner的promise
click //outer的click
promise //outer的promise
timeout //inner的timeout
timeout //outer的timeout
我覺得解釋應該是這樣的:
1、開始執行,因為事件冒泡的緣故,事件觸發執行緒會將向上派發事件的任務放入任務佇列,接著執行,列印了click,把timeout放入任務佇列,把promise放入了微任務佇列,
2、執行堆疊清空,check微任務佇列,發現微任務,列印promise,第一遍事件回圈結束,
3、從任務佇列里取出任務,執行outer的click事件,列印click,把outer的timeout放入任務佇列,把outer的promise放入了微任務佇列,執行inner放入任務佇列的timeout,
4、執行堆疊清空,check微任務佇列,發現微任務,列印promise,第二遍事件回圈結束,
5、從任務佇列里取出任務,把timeout列印出來,
JS觸發上面的click事件
一樣的代碼,只不過用JS觸發結果就會不一樣,
對代碼做了稍稍改變,將click拆分成兩個方法,方便追蹤是誰被觸發了,
var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');
const onInnerClick = (e) => {
console.log('inner cilcked');
setTimeout(function() {
console.log('inner timeout');
}, 0);
Promise.resolve().then(function() {
console.log('inner promise');
});
}
const onOuterClick = (e) => {
console.log('outer clicked');
setTimeout(function() {
console.log('outer timeout');
}, 0);
Promise.resolve().then(function() {
console.log('outer promise');
});
}
inner.addEventListener('click', onInnerClick);
outer.addEventListener('click', onOuterClick);
inner.click();
執行結果:
inner cilcked
outer clicked
inner promise
outer promise
inner timeout
outer timeout
之所以會出現這樣的差異,我的理解是JS代碼執行中的click事件,分發了一個同步的冒泡事件,所以在第一個click事件結束之后,呼叫堆疊中有outer的click事件,所以出現了兩個連續的click,
這也是根據結果猜測程序,心里沒底,
參考資料:
什么是 Event Loop?
Tasks, microtasks, queues and schedules
js中的宏任務與微任務
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/281990.html
標籤:JavaScript
上一篇:JS原生2048小游戲原始碼分享
