從設錯參數來學習 async 與 sync 的差異

真的有夠鬧。

來龍去脈

這個錯誤是來自「程式導師實驗計畫第五期」week8 的進階挑戰題

原本我是想做出「每抓到一筆資料就更新 DOM 來顯示獎品數量」,像這樣:

goal

但我犯了一個很蠢的錯誤,就是把 XMLHttpRequest.send() 的第三個參數(async)寫錯,讓每一次 request 都變成「同步」的,所以就得到了滿滿的「block(堵塞)」,也體會到了 you can’t do anything, because it’s stuck. 這句話的內涵。

原始碼:

(有興趣的人可以直接貼到 console 去跑跑看)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// 測試用的按鈕
const button = document.querySelector('button')
// 加上 click 事件,如果發生 block 就點不了
button.addEventListener('click', () => console.log('click'))

/*
以下是發 request 的流程
total: 總共要發幾次 request
counter: 紀錄發了幾次 request
當 counter < total 就會繼續發出 request
*/
let total = 1000
let counter = 1
function handler () {
// counter + 1(更新)
counter++
// 檢查回傳資料
if (request.status >= 200 && request.status < 400) {
const { prize, error=null } = JSON.parse(request.responseText)
if (error) {
console.log(error)
}
switch (prize) {
case 'NONE':
console.log(prize)
break
case 'FIRST':
console.log(prize)
break
case 'SECOND':
console.log(prize)
break
case 'THIRD':
console.log(prize)
break
}
} else {
// 發生未預期的錯誤
console.log('500')
}
// 沒抓夠,在 send 一次 request
if (counter <= total) {
/*
問題就出在這裡
request.send() 的第三個參數
true: 非同步執行
false: 同步執行
*/
request.open(
'GET',
'https://dvwhnbka7d.execute-api.us-east-1.amazonaws.com/default/lottery',
false
)
// 因為是同步,所以會卡在這裡等
request.send()
}

}
const request = new XMLHttpRequest()
request.addEventListener('load', handler)
request.open(
'GET',
'https://dvwhnbka7d.execute-api.us-east-1.amazonaws.com/default/lottery',
false
)
// 因為是同步,所以會卡在這裡等
request.send()
// 等上面全部執行完,才會跑這一段
console.log('sync')

輸出:

mistake

可以注意到 clicksync 會等到「所有的 request」結束後才被執行到。

當時為了找出原因,我還試著用 setTimeout 來模擬一段非同步操作。

原始碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*
以下會是每一次的 callback (handler)
去呼叫下一個 setTimeout
並且把目前的數字顯示到 DOM 元素上
會從 1 跑到 1000 為止
*/
let total = 1000
let counter = 1
function handler () {
// 更新 DOM 元素內容
div.innerText = counter++
// 還沒 1000 次就在呼叫一次 setTimeout
if (counter <= total) {
setTimeout(handler, 100)
}
}
// 第一次執行 setTimeout
setTimeout(handler, 100)

輸出:

testing

這下我更亂了,明明兩個的邏輯是一樣的卻有不同的行為?

後來我試著用 debugger 來看到底哪裡出了問題,但還是沒找出來。直到最後在 starkoverflow 的 XMLHttpRequest in for loop 才發現:

靠北,原來是我寫錯參數。

所以把只要把參數改掉,一切就正常了:

原始碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// 測試用的按鈕
const button = document.querySelector('button')
// 加上 click 事件,如果發生 block 就點不了
button.addEventListener('click', () => console.log('click'))

/*
以下是發 request 的流程
total: 總共要發幾次 request
counter: 紀錄發了幾次 request
當 counter < total 就會繼續發出 request
*/
let total = 1000
let counter = 1
function handler () {
// counter + 1(更新)
counter++
// 檢查回傳資料
if (request.status >= 200 && request.status < 400) {
const { prize, error=null } = JSON.parse(request.responseText)
if (error) {
console.log(error)
}
switch (prize) {
case 'NONE':
console.log(prize)
break
case 'FIRST':
console.log(prize)
break
case 'SECOND':
console.log(prize)
break
case 'THIRD':
console.log(prize)
break
}
} else {
// 發生未預期的錯誤
console.log('500')
}
// 沒抓夠,在 send 一次 request
if (counter <= total) {
/*
問題就出在這裡
request.send() 的第三個參數
true: 非同步執行
false: 同步執行
*/
request.open(
'GET',
'https://dvwhnbka7d.execute-api.us-east-1.amazonaws.com/default/lottery',
true
)
// 非同步執行,會接著跑下面的程式碼
request.send()
}

}
const request = new XMLHttpRequest()
request.addEventListener('load', handler)
/*
問題就出在這裡
request.send() 的第三個參數
true: 非同步執行
false: 同步執行
*/
request.open(
'GET',
'https://dvwhnbka7d.execute-api.us-east-1.amazonaws.com/default/lottery',
true
)
// 非同步執行,會接著跑下面的程式碼
request.send()
// 上面被 call 完後馬上跑這一段
console.log('sync')

輸出:

correct

現在一開始 sync 就會被執行,而且 click 也能正常執行,不會有像剛剛「block」的情況發生。

後記

雖然搞了場烏龍,但我們可以從中學到一點東西,因此我花了些時間來分析這兩者實際上的執行流程。

在同步的情況時:

sync

簡單來說,用來處理 response 的 handler 都會去執行下一個 request:request.send()

所以第一個 request.send 會一直等一直等,等到最後的 request 被解決完,在「遞迴」到最一開始的原點。但是在那之前會先得到 Maximum call stack 的錯誤。(佔太多空間啦)

在非同步的情況時:

async

這裡最大的特點是,每當 Call stack 中執行到「非同步」時,就會像圖中那樣直接丟到「Web API」讓瀏覽器去處理,此時 Call stack 就「空出位置」能夠執行其他的程式碼。(這也是為什麼 syncclick 可以正常執行)

另外每一次 handler 在執行下一個 request 的時候,因為是非同步,所以 request 會直接被丟到 Web API 處理,而 handler 就執行結束了,不會像同步的情況一直「堆疊」。

mentor-program-day45 mentor-program-day44
Your browser is out-of-date!

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

×