從幾道題目帶你深入理解Event Loop_宏隊列_微隊列
- 2021 年 5 月 16 日
- 筆記
- javascript
深入探究JavaScript的Event Loop
Javascript是一門單執行緒語言
但是在運行時難免會遇到需要較長執行時間的任務如: 向後端伺服器發送請求。 其他的任務不可能都等它執行完才執行的(同步)否則效率太低了, 於是非同步的概念就此產生: 當遇到需要較長時間的任務時將其放入”某個地方”後繼續執行其他同步任務, 等所有同步任務執行完畢後再poll(輪詢)剛剛這些需要較長時間的任務並得到其結果
而處理非同步任務的這一套流程就叫Event Loop
即事件循環,是瀏覽器或Node的一種解決javaScript單執行緒運行時不會阻塞的一種機制, 於是更完善的說法是: Javascript是一門單執行緒非阻塞語言
Event Loop的結構
- 堆(heap): 用於存放JS對象的數據結構
- 調用棧(stack): 同步任務會按順序在調用棧中等待主執行緒依次執行
- Web API: 是瀏覽器/Node 用於處理非同步任務的地方
- 回調隊列(callbacks queue): 經過Web API處理好的非同步任務會被一次放入回調隊列中, 等一定條件成立後被逐個poll(輪詢)放入stack中被主執行緒執行
回調隊列(callbacks queue)的分類
回調隊列(callbacks queue)進而可以細分為
-
宏任務(macroTasks)
- script全部程式碼、
- setTimeout、
- setInterval、
- setImmediate(瀏覽器暫時不支援,只有IE10支援,具體可見MDN)、
- I/O、UI Rendering
-
微任務(microTasks)
- Process.nextTick(Node獨有)
- MutationObserver
- Promise、
- Object.observe(廢棄)
Event Loop的執行順序
- 首先順序執行初始化程式碼(run script), 同步程式碼放入調用棧中執行, 非同步程式碼放入對應的隊列中
- 所有同步程式碼執行完畢後,確認調用棧(stack)是否為空, 只有stack為為空才能開始按照隊列的特性輪詢執行 微任務隊列中的程式碼
- 只有當所有微任務隊列中的任務執行完後, 才能執行宏任務隊列中的下一個任務
用流程圖表示:
通過題目來深入
題目1:
setTimeout(() => {
console.log(1)
}, 0)
Promise.resolve().then(
() => {
console.log(2)
}
)
Promise.resolve().then(
() => {
console.log(4)
}
)
console.log(3)
-
執行初始化程式碼
-
初始化程式碼執行完畢, 調用棧為空所以可以開始輪詢執行微任務隊列的程式碼
-
取出第一個任務到調用棧–列印2, 執行完後調用棧為空, 檢查微任務隊列是否還有任務有則執行
-
取出第二個任務到調用棧–列印4, 執行完後調用棧為空, 微任務隊列為空, 第一個宏任務(run script)完成, 可以輪詢宏任務隊列的下一個任務
-
-
開始輪詢執行宏任務隊列中的下一個任務
於是這道題最終的結果是:
3 2 4 1
到這需要說明一個東西就是: setTimeout的回調執行是不算在run script中的, 具體原因我並未弄清, 有明白的同學歡迎解釋
題目2:
setTimeout(()=>{
console.log(1)
}, 0)
new Promise((resolve, reject) => {
console.log(2)
resolve()
})
.then(
() => {
console.log(3)
}
)
.then(
() => {
console.log(4)
}
)
console.log(5)
-
執行初始化程式碼
-
初始化程式碼執行完畢, 調用棧為空所以可以開始輪詢執行微任務隊列的程式碼
- 取出第一個任務到調用棧–列印3, 執行完後調用棧為空, 此時第一個then()返回的Promise有了狀態、結果, 於是將第二個then()放入微任務隊列中, 檢查微任務隊列是否還有任務有則執行
- 調用棧、微任務隊列為空, 宏任務run script執行完畢
- 取出第一個任務到調用棧–列印3, 執行完後調用棧為空, 此時第一個then()返回的Promise有了狀態、結果, 於是將第二個then()放入微任務隊列中, 檢查微任務隊列是否還有任務有則執行
-
開始輪詢執行宏任務隊列中的下一個任務
於是這道題最終的結果是:
2 5 3 4 1
題目3:
const first = () => {
return new Promise((resolve, reject) => {
console.log(3)
let p = new Promise((resolve, reject) => {
console.log(7)
setTimeout(() => {
console.log(5)
}, 0)
resolve(1)
})
resolve(2)
p.then(
arg => {
console.log(arg)
}
)
})
}
first().then(
arg => {
console.log(arg)
}
)
console.log(4)
-
執行初始化程式碼
-
初始化程式碼執行完畢, 調用棧為空所以可以開始輪詢執行微任務隊列的程式碼
- 取出第一個任務到調用棧–列印1, 執行完後調用棧為空, 檢查微任務隊列是否還有任務有則執行
- 調用棧、微任務隊列為空, 宏任務run script執行完畢
- 取出第一個任務到調用棧–列印1, 執行完後調用棧為空, 檢查微任務隊列是否還有任務有則執行
-
開始輪詢執行宏任務隊列中的下一個任務
於是這道題最終的結果是:
3 7 4 1 2 5
題目4:
setTimeout(()=>{
console.log(0)
}, 0)
new Promise((resolve, reject) => {
console.log(1)
resolve()
})
.then(
() => {
console.log(2)
new Promise((resolve, reject) => {
console.log(3)
resolve()
})
.then(
() => console.log(4)
)
.then(
() => console.log(5)
)
}
)
.then(
() => console.log(6)
)
new Promise((resolve, reject) => {
console.log(7)
resolve()
})
.then(
() => console.log(8)
)
-
執行初始化程式碼
-
初始化程式碼執行完畢, 調用棧為空所以可以開始輪詢執行微任務隊列的程式碼
- 取出第一個任務到調用棧–執行onResolved中的所有程式碼, 很重要的地方是此時第一個new Promise的第二個then此時會被放入微任務隊列中。 執行完後調用棧為空, 檢查微任務隊列是否還有任務有則執行
- 調用棧、微任務隊列為空, 宏任務run script執行完畢
- 取出第一個任務到調用棧–執行onResolved中的所有程式碼, 很重要的地方是此時第一個new Promise的第二個then此時會被放入微任務隊列中。 執行完後調用棧為空, 檢查微任務隊列是否還有任務有則執行
-
開始輪詢執行宏任務隊列中的下一個任務
於是這道題最終的結果是:
1 7 2 3 8 4 6 5 0
題目5:
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function () {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function () {
console.log('promise1')
})
.then(function () {
console.log('promise2')
})
console.log('script end')
-
執行初始化程式碼
-
初始化程式碼執行完畢, 調用棧為空所以可以開始輪詢執行微任務隊列的程式碼
- 取出第一個任務到調用棧–執行await後的所有程式碼, 執行完後調用棧為空, 檢查微任務隊列是否還有任務有則執行
- 調用棧、微任務隊列為空, 宏任務run script執行完畢
- 取出第一個任務到調用棧–執行await後的所有程式碼, 執行完後調用棧為空, 檢查微任務隊列是否還有任務有則執行
-
開始輪詢執行宏任務隊列中的下一個任務
於是這道題最終的結果是:
script start
async2 end
Promise
script end
async1 end
promise1
promise2
setTimeout
終極題1:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<style>
.outer {
width: 200px;
height: 200px;
background-color: orange;
}
.inner {
width: 100px;
height: 100px;
background-color: salmon;
}
</style>
</head>
<body>
<div class="outer">
<div class="inner"></div>
</div>
<script>
var outer = document.querySelector('.outer')
var inner = document.querySelector('.inner')
new MutationObserver(function () {
console.log('mutate')
}).observe(outer, {
attributes: true,
})
function onClick() {
console.log('click')
setTimeout(function () {
console.log('timeout')
}, 0)
Promise.resolve().then(function () {
console.log('promise')
})
outer.setAttribute('data-random', Math.random())
}
inner.addEventListener('click', onClick)
outer.addEventListener('click', onClick)
</script>
</body>
</html>
-
執行初始化程式碼
-
初始化程式碼執行完畢, 調用棧為空所以可以開始輪詢執行微任務隊列的程式碼
- 取出第一個任務到調用棧–列印promise, 執行完後調用棧為空, 檢查微任務隊列是否還有任務有則執行
- 調用棧、微任務隊列為空, 因為存在冒泡, 所以以上操作再進行一次
- 取出第一個任務到調用棧–列印promise, 執行完後調用棧為空, 檢查微任務隊列是否還有任務有則執行
-
宏任務run script執行完畢, 調用棧、微任務隊列為空可以輪詢執行宏任務隊列中的下一個任務
-
開始輪詢執行宏任務隊列中的下一個任務
-
微任務隊列、調用棧為空, 繼續輪詢執行宏任務隊列中的下一個任務
於是這道題最終的結果是:
click
promise
mutate
click
promise
mutate
timeout
timeout
不同瀏覽器下的不同結果(如果你的結果在這其中, 也是對的)
這裡令人迷惑的點是: outer的冒泡執行為什麼比outer的setTimeout先
那是因為:
- 首先outer的setTimeout是一個宏任務, 它進入宏任務隊列時是在了run script的後面
- inner執行到mutate後run script並沒有執行完, 而是還有一個outer.click的冒泡要執行
- 只有執行完該冒泡後, run script才真正執行完(才可以執行下一個宏任務)
終極題2:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<style>
.outer {
width: 200px;
height: 200px;
background-color: orange;
}
.inner {
width: 100px;
height: 100px;
background-color: salmon;
}
</style>
</head>
<body>
<div class="outer">
<div class="inner"></div>
</div>
<script>
var outer = document.querySelector('.outer')
var inner = document.querySelector('.inner')
new MutationObserver(function () {
console.log('mutate')
}).observe(outer, {
attributes: true,
})
function onClick() {
console.log('click')
setTimeout(function () {
console.log('timeout')
}, 0)
Promise.resolve().then(function () {
console.log('promise')
})
outer.setAttribute('data-random', Math.random())
}
inner.addEventListener('click', onClick)
outer.addEventListener('click', onClick)
inner.click() // 模擬點擊inner
</script>
</body>
</html>
-
執行初始化程式碼, 這裡與終極題1不同的地方在於: 終極題1的click是作為回調函數(dispatch), 而這裡是直接同步調用的
-
inner.click執行完畢, inner.click退棧, 由於調用棧並不為空, 所以不能輪詢微任務隊列, 而是繼續執行run script(執行冒泡部分)
需要注意: 由於outer.click的MutationObserver並未執行所以不會被再次添加進微任務隊列中
-
inner.click退棧, 宏任務run script執行完畢, run script也退棧 調用棧為空, 開始輪詢微任務隊列
-
調用棧、微任務隊列為空, 開始輪詢執行宏任務隊列中的下一個任務
-
微任務隊列、調用棧為空, 繼續輪詢執行宏任務隊列中的下一個任務
於是這道題最終的結果是:
click
click
promise
mutate
promise
timeout
timeout
參考文章:
Tasks, microtasks, queues and schedules