await 的等待機制跟我想的不太一樣

真神奇。

簡述

首先以下都只是我個人實驗出的結果,並根據實驗結果來得出的「結論」。沒有參考規範,也毫無明確性可言(也許只是我唬爛)。所以當作參考就好,我的觀點很有可能是錯的。

範例

先來看一般的同步處理,瀏覽器在處理同步段落的時候會出現「freeze(凍結)」的情境,必須等到同步執行完後才「unfreeze(解凍)」:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
~async function() {
// 監聽 click 事件
document
.querySelector('button')
.addEventListener('click', () => {
console.log('click')
})

// 同步跑迴圈,陷入 freeze 狀態
// 所有的 click 都會被放在 callback queue
for (let i=0; i<=10000000000; i++) {

}

// 等到迴圈跑完 click 才會全部蹦出來!
console.log('loop finished')
}()

Output:

real-sync

關於同步行為在 Event loop 的機制可以參考這張精美的圖:

flow-1

接下來是 await 等待 Promise 執行完後的處理。

我原本預期會跟上面一樣,因為 await 不就是把非同步變成同步嗎?它必須等到 resolve 後才執行後面的程式碼,但實驗結果是出乎預料的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
~async function() {
document
.querySelector('button')
.addEventListener('click', () => {
console.log('click')
})
// 如果這裡也變成同步,
// 那應該會跟剛剛一樣出現 freeze 的情況
await new Promise((resolve, reject) => {
setTimeout(() => {
console.log('finished')
resolve()
}, 1000 * 5)
})
// 等 Promise 跑完才執行
console.log('yo')
}()

Output:

fake-sync

結論

其實我沒有很懂背後的原理是什麼,不過從結果來看似乎 await 並不是真的把 Promise 從「非同步」變成「同步」,而是有某種機制可以讓 resolve 執行完後才接著跑,而最重要的是這個機制不會讓瀏覽器被凍結住。

題外話,會無聊做這實驗是因為學了 await 後突然回想起同步的概念。同步的特性是會讓瀏覽器「卡在哪裡等」。所以推測如果真是如此的話,那不就很有可能在等某個 Promise 結束前都被「卡住」嗎?這樣不會有問題嗎?因此就抱著好奇的心來測試看,做了這個小實驗。

最後不得不說設計出這些東西的人真的很強大。

後記

(原本的推導過程也蠻有趣的,所以就留著不修正了)

後來上網查之後理解了,原來 await 只是把 Promise 包裝起來的語法糖。意思是說 await 只是讓你「看起來好像是同步」,但背後其實是幫你把東西放到 .then() 裡面一個一個執行,參考下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
~async function() {
document
.querySelector('button')
.addEventListener('click', () => {
console.log('click')
})
// 看起來好像是同步,先等這行跑完
await waitFiveSeconds()
// 才執行這行
console.log('yo')

}()

function waitFiveSeconds() {
return new Promise((resolve, reject) => {
setTimeout(function() {
resolve()
}, 1000 * 5)
})
}

但把糖果拆開後就發現一樣是 .then

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
~async function() {
document
.querySelector('button')
.addEventListener('click', () => {
console.log('click')
})
/*
.then: setTimeout 結束,resolve 沒有帶參數所以回傳值是 undefined
.then: 下一行要執行的 'yo'
*/
waitFiveSeconds()
.then(() => undefined)
.then(() => console.log('yo'))

}()

function waitFiveSeconds() {
return new Promise((resolve, reject) => {
setTimeout(function() {
resolve()
}, 1000 * 5)
})
}

所以這也是為什麼 await 一定要放在 async 函式裡面,因為在裡面它才能夠把「非同步」用 Promise 包裝起來,讓你感覺像是「同步」執行一樣,但其實背後還是用 .then 一行一行來執行的。

最後在附上一個小範例,看你能不能猜出正確的執行順序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
~async function() {
// 同步
console.log('sync')
// 非同步
document
.querySelector('button')
.addEventListener('click', () => {
console.log('click')
})
// 非同步,但用 Promise 包起來控制執行順序
// .then(() => undefined)
await new Promise((resolve, reject) => {
setTimeout(() => {
console.log('finished')
resolve()
}, 1000 * 5)
})
// .then(() => console.log(yo))
console.log('yo')
}()
// 同步
console.log('haha')

答案是:

1
2
3
4
sync
haha
finished
yo
mentor-program-day42 mentor-program-day41
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×