[JS] 同步非同步霧煞煞(上)-AJAX

[JS] 同步非同步霧煞煞(上)-AJAX

前言

接觸Js一段時間後,開始學習如何串接API,剛開始時就直接先被介紹用了Axios這個好用的套件,但原理等等還是不能理解,因此看了很多文章文件,才摸索到原來同步,非同步,Event Queue,Ajax,XMLHttpRequest,fetch,promise,Async,Await等等之間的關聯性,才把觀念串通。這篇會來把我所認知的概念整理起來。

同步Synchronous與非同步Asynchronous

Javascript是單執行緒 (single threaded)、同步(Synchronous) 的程式語言,一次只能做一件事,但為何google上很多人都說Javascript是非同步呢?其實是使用setTimeout、AJAX、Promise等而有非同步的特性,但其實這些方法並不是由javascript程式語言本身提供的,不受單執行緒限制,他們會被放到Event Queue中。

我們目前常用的Javascript執行環境有瀏覽器和Node.js。在瀏覽器上透過document來操作DOM,document也不是程式語言本身提供,而是瀏覽器,因此click這個事件監聽也是非同步特性。在瀏覽器上其他方法還有做AJAX的 XMLHttpRequest, fetch, Promise,計時用的setTimeout和setInterval。執行環境用 <script src="all.js"> 引入在html上。而在Node.js上則有http可做Ajax寫伺服器, os, fs來控制檔案介面,同時也有SetTimeout。執行方式安裝node.js用node all.js來執行。

以前大學的暑假,曾經在美國的麥當勞工作過,櫃檯工作的流程就很像是非同步的模式。覺得中文翻譯過來的非同步很容易讓人誤解成不能同步工作。同步應該要翻成只能一步一步做,上一步會阻礙下一步(blocking特性),非同步則為可以分開做…不會阻礙下一步(non-blocking特性),同步非同步這個字義讓一開始的我一直搞混。

同步synchronous模式

  1. 櫃檯小姐把你要的餐點輸入點餐機: 起司漢堡一個、薯條一份
  2. 櫃檯小姐把Order傳送給漢堡流水線去要漢堡
  3. 等待漢堡完成
  4. 漢堡製作好了,櫃檯小姐給你
  5. 櫃檯小姐把Order傳送給薯條台去要薯條
  6. 等待薯條完成
  7. 薯條製作好了,櫃檯小姐給你
  8. 結帳

非同步Asynchronous模式

櫃檯小姐 (Call Stack)

  1. 櫃檯小姐把你要的餐點輸入點餐機: 起司漢堡一個、薯條一份
  2. 櫃檯小姐把Order傳送給漢堡流水線去要漢堡 (WebAPIs Request)
  3. 櫃檯小姐把Order傳送給薯條台去要薯條 (WebAPIs Request)
  4. 櫃檯小姐幫你結帳
  5. 收到薯條台通知,薯條炸好了
  6. 結帳完成
  7. 櫃檯小姐沒事了,出餐口機器手臂把薯條給櫃檯小姐後,放在給你的餐盤上 (callback function)
  8. 收到廚房通知說漢堡做好了
  9. 櫃檯小姐沒事了,出餐口機器手臂把漢堡給櫃檯小姐後,放在給你的餐盤上 (callback function)
  10. 出餐完成

–廚房端和出餐口(Event Queue)

廚房端 (WebAPIs)

  1. 漢堡流水線和薯條台接收到櫃檯小姐的Order
  2. 各自開始製作,漢堡流水線中有人去上廁所需要3分鐘才會開始製作(setTimeout 3分鐘)
  3. 各部門製作好後各自送到出餐口去(response),製作失敗(error)

出餐口 (Callback Queue or Task Queue)

  1. 薯條製作好了,等待櫃檯小姐拿取,但他好像還在忙喔!
  2. 出餐口機器手臂(Event Loop)偵測到櫃檯小姐結帳完了沒事(Call Stack空了),把薯條給櫃檯小姐
  3. 漢堡製作好了,等待櫃檯小姐拿取
  4. 出餐口機器手臂(Event Loop)偵測到櫃檯小姐給完薯條了沒事(Call Stack空了),把漢堡給櫃檯小姐

以下例子取自Tommy大的教學,非同步最常見的例子,setTimeOut設定為0秒但還是會被排到Event Queue中等待console.log(‘C’)執行完後才會再執行,因此會得到 A C B的結果。

1
2
3
4
5
6
7
8
9
10
console.log('A');

function run() {
console.log('B');
}
setTimeout(run, 0);

console.log('C');

// A C B

下面這個例子,setTimeOut設定為3秒,下方接一個5秒回圈後執行C,在5秒的過程中setTimeout在經過3秒後便開始在WebApis端執行並把結果放在Callback Queue中,等C一執行完成後Call Stack空了,EventLoop變馬上把Callback Function放入Stack中,B則馬上跳出。

1
2
3
4
5
6
7
8
9
10
11
12
console.log('A');
function run() {
console.log('B');
}
setTimeout(run, 3000);

var start = Date.now();
while (Date.now() - start <= 5000) {}
// 讓迴圈重複一直跑,5 秒後才跳出執行下段
console.log('c');

// A 經過5秒後 C B

AJAX (Asynchronous JavaScript and XML)

直接翻譯過來就是 非同步的 JavaScript 與 XML 技術,可以即時向伺服器傳送
(request)並取回必須的資料(response),XML則是一種早期比較常在用的資料格式,現在比較多人使用的是JSON格式,不知道這樣AJAX要不要改成AJAJ…XD

伺服器request的底層方法:

衍生的request的library套件:

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

這邊會用隨機用戶產生器來做練習

XMLHttpRequest (可支援 IE 7 以上)

原生的JS,但缺點是寫法會比較難閱讀和複雜

XMLHttpRequest DEMO 非同步 GET 請求

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var xhr = new XMLHttpRequest();
const url = 'https://randomuser.me/api/';

xhr.addEventListener('load', function(){
const data = JSON.parse(this.responseText);
console.log(this.responseText);
console.log(this.status); //拿到 Http status code 狀態碼
}); //xhr.onload = function(){ … }
xhr.addEventListener('error', function(error){
console.log('錯誤', error);
});//xhr.onerror = function(){ … }

xhr.open('GET', url, true); //預設非同步true 可不寫
xhr.send();

DEMO 同步 GET 請求

1
2
3
4
5
6
7
8
9
10
console.log('start');
var xhr = new XMLHttpRequest();
const url = 'https://randomuser.me/api/';
xhr.open('GET', url, false); //同步:false 非同步:true
xhr.send();
console.log(xhr.responseText);
console.log('end');
// 'start'
// responseText
// 'end'

DEMO 非同步 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 傳送到後端
1
2
3
4
5
6
7
8
const xhr = new XMLHttpRequest();
xhr.open('POST', url);
xhr.setRequestHeader('Content-type', 'application/json');
xhr.send(JSON.stringify(user));
xhr.onload = function () {
const res = JSON.parse(xhr.response);
console.log(res.message);
};

HTML5 Fetch API (IE 11 以下都不支援)

MDN文件

ES6的新語法,搭配 Promise 作回應,支持 async/await.then()接續.catch()接收錯誤,回傳的是 ReadableStream 物件,需要使用不同資料類型(json,blob)使用對應方法,才能正確取得資料物件。 但使用上也有一些需要注意的:

  1. IE 11以下都不支援
  2. 回傳的 promise 不會 reject HTTP 的 error status,就算是 HTTP 404 或 500 也一樣。相反地,它會正常地 resolve,並把 ok status 設為 false。會讓它發生 reject 的只有網路錯誤或其他會中斷 request 的情況,需要封裝去處理。
  3. 預設是沒有帶cookie,需要新增配置項。
  4. 不支援超時控制,使用setTimeout及Promise.reject的實現的超時控制並不能阻止請求過程繼續在後臺執行,造成了量的浪費。
  5. 不支援原生監測請求的進度,而XHR可以

Fetch 常用的 Request 屬性

url: 第一個參數,一定要填的項目,代表需要 fetch 對象的網址
method: GET、POST、PUT、DELETE、HEAD ( 預設 GET )
headers: 要求相關的 Headers 物件 ( 預設 {} )
mode: cors、no-cors、same-origin、navigate ( 預設 cors )
referrer: no-referrer、client 或某個網址 ( 預設 client )
credentials: omit、same-origin、include ( 預設 omit )
redirect: follow、error、manual ( 預設 manual )
cache: default、no-store、reload、no-cache、force-cache ( 預設 default )
body: 要加到要求中的內容 ( 如果 method 為 GET 或 HEAD 則不設定 )

Fetch 常用的 Response 屬性

headers: 包含與 response 相關的 Headers 物件
ok: 成功回傳 true,不成功回傳 false
status: 狀態代碼,成功為 200
statusText: 狀態文字,成功為 ok
type: response 的類型,例如 basic、cors…等
url: response 的 url

ReadableStream 物件

Fetch API 的 回傳為 ReadableStream 物件,我們無法直接讀取資料內容,而 ReadableStream 物件中可用以下對應的方法來取得資料 (https://developer.mozilla.org/zh-TW/docs/Web/API/Body):

arrayBuffer() 返回 Promise,Buffer,代表一段記憶體區塊,僅能透過 View 操作其內容。
formData() 返回 Promise,取得的資料格式將會是formData格式
json() 返回 Promise,取得的資料格式將會是JSON格式
text() 返回 Promise,取得的資料格式將會是純字串
blob() 返回 Promise,
可以將資料轉為blob物件,像是圖片就可以做這樣的轉換
(這裡的圖片並非指圖片路徑,而是圖片檔案本身)。

clone() 建立 Response 的複製物件
error() 返回 Response 的錯誤物件

1
2
3
4
5
6
7
8
9
10
11
12
// unsplash 上的圖片
let url = 'https://images.unsplash.com/photo-1513313778780-9ae4807465f0?auto=format&fit=crop&w=634&q=80'
fetch(url)
.then((response) => {
return response.blob();
})
.then((imageBlob) => {
let img = document.createElement('IMG')
document.querySelector('.newImg').appendChild(img);
// 將 blog 物件轉為 url
img.src = URL.createObjectURL(imageBlob);
})

Fetch DEMO 發送 GET 請求

1
2
3
4
5
6
7
8
9
10
const url = 'https://randomuser.me/api/';
fetch(url).then(response => { //預設method:get
// 這裡會得到一個 ReadableStream 的物件
return response.json();
// 可以透過 blob(), json(), text() 轉成可用的資訊
})
.then(data =>
console.log(data);) // 取得資料
.catch((err) => {
console.log('錯誤:', err);

Fetch DEMO 發送 POST 請求

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

fetch('url', {
method: 'POST',
body: data,
}) // 如果是傳遞「中文」可能會出現亂碼,這時可以使用encodeURI來做轉碼,且要透過JSON.stringify來轉換成 string 方式傳遞。 encodeURI(JSON.stringify())
.then(response => response.json()) // 解讀JSON格式
.then(data =>
console.log(data)) // 取得資料
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let url = `https://hexschool-tutorial.herokuapp.com/api/${type}`;
let user = {
email: document.querySelector(`#email_${type}`).value,
password: document.querySelector(`#password_${type}`).value,
};

fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(user),
})
.then(response => response.json())
.then(data => {
console.log(data);
})
.catch(err => console.log(err));

jQuery Ajax

以XMLHttpRequest為基礎的library,使用簡單,但jQuery本身是針對MVC的程式設計,不符合現在前端MVVM的浪潮,因為現在有許多新的原生語法可取代JQuery,若單純只是使用Ajax卻引入整個jquery不太合理。

DEMO 發送 GET 請求

1
2
3
4
5
6
7
8
9
10
11
12
$.ajax({
type : "GET", //預設get,如果要使用 POST 方法則換成POST
url: 'https://randomuser.me/api/',
dataType: 'json',
success: function(data) {
console.log(data);
},
error : function(error){
console.log(error.status);
console.log(error.responseText);
}
});
1
2
3
4
5
$.ajax({
url: 'https://randomuser.me/api/'
}).done(function(res){
console.log(res);
});

axios

axios 本質上也是對原生的 XMLHttpRequest 封裝,從 node.js 建立 http 請求,並可以支援 Promise,可取消請求(Promise無法),自動轉換JSON檔,客戶端支援防止CSRF,提供了一些併發請求的介面,axios 實體建立

DEMO 發送 GET 請求

1
2
3
4
5
6
7
8
const url = 'https://randomuser.me/api/';
axios.get('url')
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});

DEMO 發送 POST 請求

1
2
3
4
5
6
7
8
const url = 'https://randomuser.me/api/';
axios.post('url')
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});

執行多個併發請求

1
2
3
4
5
6
7
8
9
10
function getData1() {
return axios.get('url1');
}
function getData2() {
return axios.get('url2');
}
axios.all([getData1(), getData2()])
.then(axios.spread(function () {
// 兩個請求現在都執行完成
}));

參考資料

[JS] 同步非同步霧煞煞(上)-AJAX

https://kaiyuncheng.github.io/2020/11/06/eventQueue/

Author

KaiYun Cheng

Posted on

2020-11-06

Updated on

2024-04-13

Licensed under

Comments

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×