0%

[JS] 講座筆記:AJAX 非同步 / Event Queue 詳解

本篇筆記來自 2020.2.19 六角學院線上研討會,講者為偷米騎巴哥的 Tommy。在這一次講座中,Tommy 老師用許多 Demo 示範了非同步行為與 Queue 之間的關係。

Event Queue 事件佇列

JavaScript 是 Single Thread(單執行緒)語言,只用一顆 CPU 運轉,一次只能做一件事。但是,計時器(setTimeout)、AJAX、Promise 是屬於 Web APIs,不受單執行緒限制,它們會被放到 Queue 中,等待 JavaScript 執行。

因此,整個流程(Event Loop)就像這樣:

所有要執行的任務會被放到 Call Stack 裡面,JavaScript 的執行緒會逐一執行 Stack 裡的任務,當碰上非同步事件時,為了不讓程式被這些需要等待的事件卡著,就會繼續執行後續的動作。

那這些非同步事件什麼時候才能被執行?答案是,它們會在 Queue 中排隊,等到 JavaScript 的執行緒把 Stack 的任務都消化完了,才輪到 Queue 中的非同步事件依序執行。

觀念重點:非同步事件會被放進排隊序列,先進先出。

單執行緒與 Event Queue 的概念可以參照鐵人賽:一次只能做一件事情的 JavaScript,有動圖更好理解。

單執行緒與 Queue

JavaScript 單執行緒會按照順序執行在 Stack 中的任務。

1
2
3
4
5
6
7
console.log(a);
function run() {
console.log('b');
}
console.log('c');

// 'a' 'b' 'c'

如果在這個例子中,穿插一個非同步事件進去,那麼執行順序會是如何呢?

1
2
3
4
5
6
7
console.log('a');
function run() {
console.log('b');
}
setTimeout(run, 3000)
console.log('c');
// 'a' 'c' 'b'

透過這個例子可以發現,非同步事件會被放到其他行為的後面執行。
我們可以把 setTimeout() 的秒數改成 0,來驗證這個結論是否為真。

1
2
3
4
5
6
7
console.log('a');
function run() {
console.log('b');
}
setTimeout(run, 0)
console.log('c');
// 'a' 'c' 'b'

秒數改成 0 以後,'c' 依然是最後才印出來,就是因為非同步事件都必須在 Queue 中等待 Stack 裡其他任務結束,才能被執行。

再來看另一個例子,也可以看出在 Queue 中的事件永遠會被放到最後才執行的特徵。

1
2
3
4
5
6
7
8
9
10
11
12
13
console.log('a');
function run() {
console.log('b');
}
setTimeout(run, 3000)
var start = Date.now();
// 讓迴圈重複一直跑,5 秒後跳出
while (Date.now() - start <= 5000) {}
console.log('c');
// 'a'
// 5 秒後
// 'c'
// 'b'

這個例子中,setTimeout() 應該在 3 秒後印出 'b',但結果卻是在 5 秒後,排在 'c' 的後面出現,為什麼呢?

這是因為,當 3 秒到時,setTimeout() 就已回來 JavaScript 的執行緒了,但在 Stack 中還有 5 秒 while 迴圈及 console.log('c'); 還沒執行完畢。
因此,當迴圈終於跑完時,'c' 接著印出來,而時間早就到了的 'b' 也就緊跟著印出來了。

for 迴圈 - 以 Vue 環境為例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// HTML
<div id="app">
<ul>
<li v-for="i in list"> {{ i }} </li>
</ul>
</div>

// JS
new Vue({
el: '#app',
data() {
return {
list: [],
}
},
mounted() {
for(let i = 0; i < 5; i++){
debugger;
this.list.push(i+1);
};
},
})

透過 debugger 可以在開發人員工具中看到迴圈的分解動作,我們可以看到 for 迴圈是等所有次數都跑完,數字才渲染出來;而不是像我們所想像的,跑一次迴圈就渲染一個數字在畫面上。

那麼,要如何每跑一次迴圈就渲染一次數字?

答案是,可以用 setTiomeiut() 來達成。

1
2
3
4
5
for(let i = 0;i < 5; i++){
setTimout(function(){
this.list.push(i+1);
}.bind(this, i)) // 指定 callback 的 this 指向外層的物件
}

為什麼用 setTiomeiut() 就能達成呢?

因為 for 迴圈一開始沒用 setTimeout() 時,只有 for 自己一個任務在 Stack 中;而使用 setTimeout() 後,就變成每跑一次迴圈就產生一個存放在 Queue 中的任務,每個 Queue 中的任務又會被依序插入 Stack 中,於是就能一個執行(渲染)完才換成下一個執行(渲染)。

AJAX

傳統網頁在跟後端撈資料時,流程是這樣的:請求 => 回應 => 請求 => 回應;而使用了 AJAX 技術的網頁撈取資料的方法,則是在背景送出請求取得回應。

常見底層

  • XMLHttpRequest (可支援 IE 7 以上, jQuery, axios)
  • HTML5 Fetch API (IE 11 以下都不支援)

測試用 API

以下兩個網站都可以用來製作測試用的 API,也可以客製化 AJAX 成功後的回傳訊息。

XMLHttpRequest DEMO

  1. 非同步 GET 請求

    1
    2
    3
    4
    5
    6
    var xhr = new XMLHttpRequest();
    xhr.addEventListener('load', function(){
    console.log(this.responseText);
    })
    xhr.open('GET', url);
    xhr.send();

    AJAX 行為會被丟到 Queue,等取得回應後才回到 JS 執行緒。

  2. 同步 GET 請求

    1
    2
    3
    4
    5
    6
    7
    8
    9
    console.log('start');
    var xhr = new XMLHttpRequest();
    xhr.open('GET', url, false);
    xhr.send();
    console.log(xhr.responseText);
    console.log('end');
    // 'start'
    // responseText
    // 'end'

    這邊只是示範同步的 AJAX 是長什麼樣,實務上盡量還是用非同步 AJAX 才能讓網頁效能較好。

  3. 非同步 POST 請求

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 如何把參數發送到後端
    var data = new FormData(); // 宣告 FormData 物件
    data.append('id', '5'); // 在 FormData 中塞入資料
    var xhr = new XMLHttpRequest();
    xhr.addEventListener('load', function(){
    console.log(this.responseText);
    })
    xhr.open('POST', url);
    xhr.send(data); // 把帶有資料的 FormData 傳送到後端

Fetch DEMO

  1. Fetch 發送 GET 請求

    1
    2
    3
    4
    5
    fetch('url').then(response => {
    return response.json(); //解讀 JSON 格式
    })
    .then(data =>
    console.log(data)) // 取得資料
  2. Fetch 發送 POST 請求

    1
    2
    3
    4
    5
    6
    7
    8
    // 把資料參數傳到後端,一樣要先宣告 FormData
    var data = new FormData();
    data.append('id', '5');

    fetch('url', { method: 'POST', body: data }) // 帶入參數
    .then(response => response.json();) // 解讀JSON格式
    .then(data =>
    console.log(data)) // 取得資料

參考資料

slide 簡報