解釋 Promise 與 Async Await

ECMAScript 對 Asynchronous 總共有 Promise、Generator 與 Async Await 三種支援,其中 Generator 屬於較進階的應用,主要是在寫 library,但 Promise 與 Async Await 則非常重要,寫 application 也很常用到。

Version


ECMAScript 2015 (Promise)
ECMAScript 2017 (Async Await)

API


productApi.js

1
2
3
4
5
6
import axios from 'axios';
import { API } from '../environment';

export default {
fetchProducts: () => axios.get(`${API}/products`),
};

實務上我們會將 API 部分另外寫在 api 目錄下,且另外寫 fetchXXX() method,但 axios.get() 回傳的到底是什麼型別呢?

是 ECMAScript 2015 新的 Promise 型別。

Promise


由於 Asynchronous 在所有 Synchronous 執行完才會執行,因此對於 AJAX 回傳的資料,對於 Synchronous 而言,屬於一種 未來值

也就是 AJAX 所回傳的資料,將來一定會有,但具體時間未知,只能先回傳 Promise 物件給你,一旦 AJAX 抓到資料,你就可以用 Promise 去換真實的資料。

async000

就類似你去麥當勞買漢堡,錢都給了,但漢堡還沒做好,但未來一定會有,也是 未來值,因此店員會給你 取餐單,將來你可以用 取餐單 去換漢堡。

取餐單 就是 Promise。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mounted() {
const response = res =>
this.products = res.data;

const error = e =>
console.log(e);

const done = () =>
console.log('finally');

productApi
.fetchProducts()
.then(response)
.catch(error)
.finally(done);
},

fetchProducts() 會回傳 Promise 物件,該物件總共有 3 個 operator (也是 Higher Order Function)。

  • then():傳入要獲取 AJAX 資料的 function,當 AJAX 抓到資料後,會自己執行 function
  • catch():傳入若 AJAX 錯誤所執行的 function,當 AJAX 出錯時,會自己執行 function
  • finally():傳入 AJAX 最後所執行的 function,當 AJAX 執行完正,會自己執行 function

finally() 目前在 ECMAScript 定義為 stage 4,也就是即將 ECMAScript 正式定義,重要是 Babel 已經率先支援,因此可安心使用

Async Await


1
2
3
4
5
6
7
8
9
10
async mounted() {
try {
const response = await productApi.fetchProducts();
this.products = response.data;
} catch (err) {
console.log(err);
} finally {
console.log('finally');
}
},

Promise 屬於 FP 觀念下的產物 (也就是 Monad Pattern),若你習慣 Imperative 思維,也可以透過 Async Await 將 Asynchronous 寫的很 Synchronous。

將 function 前面宣告 async,表示此為 Asynchronous Function,也就是內部將使用 await

responseproductApi.fetchProducts() 所回傳的 Promise,是 未來值觀念上 會 await 等 response 成真後才會繼續執行。

因為看起來很像 Synchronous 寫法,因此可以使用原本的 try catch finally

Async Await 只是程式碼看起來很像 Synchronous,但起本質仍然是 Asynchronous,因為 await 一定要對方回傳 Promise 才能使用,所以是百分之百的 Syntax Sugar

Async Await 來自於 C# 5,在 ECMAScript 2017 正式定案,Babel 也完美支援

Why Promise ?


由 JavaScript 的 Event Loop Model 可知,有三種屬於 Asynchronous:

  • DOM
  • AJAX (XMLHttpRequest)
  • setTimeout()

由於前端一定要使用 AJAX 呼叫 API,這屬於 Asynchronous 行為,會被安排在 Callback Queue ,等 Synchronous 執行完,最後才執行 Asynchronous。

在 ES5 之前,若 Asynchronous 之間有相依的先後關係,在 jQuery + Callback 只能這樣寫:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$.get('/products', (err, res) => {
if (err)
console.log(err);
else {
const product = ret.json();
$.get('/product/${ product[0].id}', (err, res) => {
if (err)
console.log(err);
else {
const item = res.json();
console.log(item);
}
});
}
});

這就是有名的 Callback Hell

  • 很容易寫出巢狀很深的 code 難以維護
  • 每個 Callback 都要自己維護 Exception
1
2
3
4
5
6
fetch('/products')
.then(res => res.json())
.then(product => fetch('/products/${ prdouct[0].id}'))
.then(res => res.json())
.then(item => console.log(item));
.catch(e => console.log(e));

使用 Promise 後:

  • 程式碼風格改成 Pipeline 方式容易閱讀
  • 每個 then() 都回傳一個全新的 Promise
  • 統一處理 Exception

Callback 雖然也能解決 Asynchronous,但會造成 Callback Hell,應盡量避免使用,且隨著 ECMAScript 2015 將 Promise 定為標準,越來越多 Library 直接回傳 Promise 型別 (Axios、Protractor …),且 Async Await 也是基於 Promise 技術,所以 Promise 已經成為不能不會的東西

Conclusion


  • Async Await 只是讓你程式碼看起來很像 Synchronous,但其本質仍然是 Asynchronous,因為 Async Await 一定要搭配 Promise,而 Promise 就是 Asynchronous,因此 Async Await 算是 Syntax Sugar
  • 雖然 Async Await 是 ECMAScript 2017 較新的東西,但個人認為 語意 其實並沒有 Promise 好,Async Await 會讓你使用 Imperative 方式思考,而 Promise 會讓你使用 Pipeline 方式思考,個人較喜歡 Promise

Sample Code


完整的範例可以在我的 GitHub 上找到

Reference


TC39, Promise.prototype.finally
MDN, Promise
MDN, async function
MDN, await

2018-10-07