關於非同步 - AJAX & Promise
AJAX (非同步的 JavaScript 與 XML 技術)
AJAX 應用可以僅向伺服器傳送並取回必須的資料,並在客戶端用 JavaScript 處理來自伺服器的回應。因為在伺服器和瀏覽器之間交換的資料大量減少,伺服器回應更快了。同時,很多的處理工作可以在發出請求的客戶端機器上完成,因此 Web 伺服器的負荷也減少了。
AJAX 技術的出現,讓瀏覽器可以向 Server 請求資料而不需費時等待。當瀏覽器接收到 response 之後,新的內容就會即時地添入原本網頁。
非同步是什麼?
發出 request 之後不需要等待 response ,可以持續處理其他事情,甚至繼續送出其他 request,等 response 傳回之後,就被融合進當下頁面或應用中。
當你在使用 DOM 事件時,其實你已經在運用非同步的概念,你不知道事件什麼時候會發生,但你可以先把要發生的函式準備好,等函式中的 callback 被呼叫的時候再執行要接著做的事。JavaScript 可以把函式當成值來傳遞,因此執行程序可以非同步的發生。
JavaScript 實現非同步的方法不斷演進著:從 callback 函式、promises 到最新的 async-await 函式。
- 所有的 AJAX 都會有對應的 API 接口,API 的接口就是一段網址,不同的網址也對應不同的「HTTP 請求方法」,請求方法必須與網址完全對應才可運作。
- AJAX 會統一由網頁端發出請求(無論新增資源或取得資源),依據不同的接口、請求方法進行請求,而伺服器會依據請求的方法、內容來進行回應。
- AJAX 會在背景送出請求取得回應,不用等待也同時可以做其他事情,等同 Queue (佇列) 的概念,收到回應才產生一個 Queue,執行並顯示在畫面。
目前常見的技術
- XMLHttpRequest (IE7 以上支援,jQuery,axios)
實務上很少直接使用原生的 XMLHttpRequest。取而代之的方法有很多種,過去較流行的是 jQuery 的 $.ajax()
,然而在 JavaScript 日趨成熟之後,許多新的替代方案應運而生:
fetch (較新,HTML5 才有,IE11 以下不支援)。fetch 並不是 XMLHttpRequest 的升級版本,不是 jQuery 提供的
$.ajax
語法或axios
那種原生 XHR 的封裝。它是一個全新的東西,並且基於 Promise 語法結構所設計,可配合使用 Async/Await 語法,使程式更加優雅。fetch('http://example.com/movies.json')
.then(function(response) {
return response.json();
})
.then(function(myJson) {
console.log(myJson);
});
async function ajax() {
const response = await fetch('http://example.com/movies.json');
const data = await response.json();
console.log(data);
}
ajax();axios 是一個使用 Promise base 的 Ajax 函式庫,他有許多重要功能如:
- 廣泛的瀏覽器支持
- 可支援 Node.js 從後端發送的 Http request,這意味著 axios 可以兼用於前端與後端專案。
- 直接將回應的 JSON 資料轉換成 JavaScript 的 Object,這十分方便!
常見的非同步問題 (不限 AJAX)
- 回呼地獄
- 寫法不一致
- 無法同時執行 (無法確定什麼時候開始及結束)
Promise
!https://miro.medium.com/max/1400/1*gtdCpCvoZ6Q-YVC-N4en2w.png
使用 promise 可以解決 1. 回呼地獄 2. 寫法不一致 3. 無法同時執行 的問題
- Promise 是為了解決傳統非同步語法難以建構及管理的問題。Promise 本身是一個建構函式。
- Promise 有三種狀態:
pending
,resolved
/fulfilled
,rejected
。 new Promise
內的函式會立即被執行,當resolve
得到內容後,才會執行.then
。- 要提供一個函式 promise 功能,讓他 return 並透過 new 方式建立一個 promise 物件,並必須傳入一個函式作為參數,帶有 resolve / reject 參數,分別代表成功及失敗的回傳結果。
- 已實現的狀態會透過 resolve 這個參數回傳一個結果,在調用時使用
.then
接收回傳結果;反之否決狀態會透過 reject 參數回傳並用.catch
接收,一次只有一種結果。 - 在
.then
的 resolvedCallback 中,可以得到在new Promise
中resolve
內所得到的值 (value)。 .then()
是 promise 的 (原型) 方法- 如果在
.then
的 resolvedCallback 中 return 一個值,則這個值會以 Promise 物件的形式傳到下一個.then
。 - HTML5 中的 Fetch API 也是使用它
基礎運用
const promiseSetTimeout = (status) => {
return new Promise((resolve, reject) => { // 傳入函式參數,帶有 resolve/reject 參數
setTimeout(() => {
if(status) {
resolve('promiseSetTimeout 成功')
} else {
reject('promiseSetTimeout 失敗')
}
}, 0);
})
}
promiseSetTimeout(true)
.then(function(res) { // .then 放入狀態完成的回調函式,針對狀態做處理
console.log(res)
})串接 (避免回呼地獄 + 巢狀寫法)
promiseSetTimeout(true)
.then(function(res) {
console.log(1, res);
return promiseSetTimeout(true) // return 下一個 promise
})
.then(res => { // return 的結果會在下一個 .then 出現
console.log(2, res)
})
promiseWrap(A)
.then(() => {
return promiseWrap(B);
})
.then(() => {
return promiseWrap(C);
})失敗的捕捉
除了成功的方法之外也要把失敗的方法補進去
promiseSetTimeout(true)
.then(res => { // 被 resolve 時執行
console.log(res);
})
.catch(err => { // 被 reject 時執行
console.log(err);
})Promise.all
所有的 promise 都回傳成功了才進入下一個任務,在此之前都是等待,但若其一回傳為失敗就進入失敗的處理狀況
Promise.all([axios.get(url), axios.get(url)])
.then([res1, res2] => {
console.log(res1, res2)
})實戰運用
const component = {
data: {},
init() {
console.log(this) // this 是指向 component
promiseSetTimeout(true)
.then(res => { // 箭頭函式會使用外層的作用域也就是指向 component
this.data.res = res; // 將回傳的 res 寫入 data 內
})
}
}
component.init();
/**
* You can get token in a Promise
* 利用 Promise 先透過 email 和 password 取得 access_key 和 secret 後,
* 再用 access_key 和 secret 取得 token。
**/
const getTokenPromise = new Promise((resolve, reject) => {
request
.post(endpoint + '/users/cert')
.send({
email: '<your_email>',
password: '<your_password>',
})
.end((err, res) => {
resolve(res);
reject(err);
});
});
getTokenPromise()
.then((response) => {
let parseResponse = JSON.parse(response.text);
console.log(parseResponse);
return new Promise((resolve, reject) => {
request
.post(endpoint + '/users/token')
.send({
access_key: parseResponse.access_key,
secret: parseResponse.secret,
})
.end((err, res) => {
resolve(res);
reject(err);
});
});
})
.then((response) => {
let parseResponse = JSON.parse(response.text);
console.log(parseResponse); // You can get Token Here
})
.catch((err) => {
console.warn('getTokenPromise with error', err);
});
async...await
只要 function 標記為 async,就表示裡頭可以撰寫 await 的同步語法,await
關鍵字只能在 async
function 中執行,而 await
就是「等待」,它會確保一個 promise 物件都解決 (resolve) 或出錯 (reject) 後才會進行下一步,當 async function 的內容全都結束後,會返回一個 promise,這表示後方可以使用 .then
語法來做連接。
在 async
函式中使用 await
關鍵字意味者:「我們請 JavaScript 等待這個非同步的作業完成,才展開後續的動作,且這個函式會回傳一個 Promise 物件」,換成 Async/Await 的話,就不必寫下 .then()
了,就像同步的程式一般,不必理會它是否為非同步。
(async function() {
await promiseWrap(A);
await promiseWrap(B);
await promiseWrap(C);
}());
另外,用迴圈處理非同步事件時,需要注意 ES6 後提供的許多 Array 方法都不支援 async / await 的語法,例如使用 forEach 取代 for,結果會變成同步執行
function getFirstInfo() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('first data')
}, 1000);
})
}
function getSecondInfo() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('second data')
}, 2000);
})
}
// 函式前加上 async 關鍵字,告知這是一個非同步函式
async function getGroupInfo() {
// 代表等到第一筆資料回傳後,才印出結果和請求第二筆資料
const firstInfo = await getFirstInfo() // 要處理非同步的地方,呼叫函式前加上 await
console.log(firstInfo)
// 代表等到第二筆資料回傳後,才印出結果
const secondInfo = await getSecondInfo() // 要處理非同步的地方,呼叫函式前加上 await
console.log(secondInfo)
}
getGroupInfo()