點燈坊

新しいことを始めるのに、遅すぎる挑戰はない

從 Callback、Promise 到 Async Await

Sam Xiao's Avatar 2019-08-26

ECMAScript 對 Asynchronous 支援比其他語言先進,從最基本的 Callback 演進到 ECMAScript 2105 的 Promise,再演進到 ECMAScript 2017 的 async await

Version

ECMAScript 5 (Callback)
ECMAScript 2015 (Promise)
ECMAScript 2017 (Async Await)

API

import axios from 'axios';
import { API } from '../environment';

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

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

是 ES6 新的 promise。

Promise

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

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

async000

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

取餐單 就是 Promise

Why Promise ?

由 ECMAScript 的 event loop model 可知,有三種屬於 asynchronous:

  • DOM
  • AJAX (XMLHttpRequest)
  • setTimeout()

由於前端一定會使用 AJAX 呼叫 API,這屬於 asynchronous 行為,會被安排在 callback queue ,等 synchronous 執行完,最後才執行 asynchronous。

Node Style Callback

ajaxGet('/products', (err, res) => {
  if (err)
    console.log(err);
  else {
    var product = res.json();
    ajaxGet('/product/${ product[0].id }', (err, res) => {
      if (err)
        console.log(err);
      else {
        var item = res.json();
        console.log(item);
      }
    });
  }
});

在 ES5 若 asynchronous 之間有相依的先後關係,只能使用 callback,這造成有名 callback hell:

  • 很容易寫出巢狀很深的 code 難以維護
  • 每個 callback 都要自己維護 exception

Promise

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))
  .finally(() => console.log('done'));

fetchProducts() 會回傳 promise,該物件總共有 3 個 method (也是 higher order function)。

  • **then()**:傳入要獲取 AJAX 資料的 callback
  • **catch()**:傳入若 AJAX 錯誤所執行的 callback
  • **finally()**:傳入 Ajax 最後所執行的 callback

使用 promise 後:

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

Async Await

try {
  let product = await fetch('/products').json();
  let item = await fetch('/products/${ prdouct[0].id }').json();
} catch (e) {
  console.log(e);
} finally {
  console.log('done');
}

Promise 屬於 FP 觀念下的產物 (其實就是 Monad),若你習慣 imperative 思維,也可以透過 async await 將 asynchronous 寫的很 synchronous。

將 function 前面宣告 async,表示此為 asynchronous function,也就是內部將使用 await 等 promise 回傳資料。

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

因為看起來很像 synchronous 寫法,也可繼續使用原本的 try...catch...finally

async await 只是程式碼看起來很像 synchronous,但起本質仍然是 asynchronous,因為 await 一定要對方回傳 promise 才能使用,所以是百分之百的 syntatic sugar

Conclusion

  • 綜觀各程式語言發展趨勢,對於 asynchronous 處理,一開始都使用最簡單的 callback 解決,但隨著 asynchronous 使用越多,callback hell 問題越明顯,便開始引入 promise 未來值概念,但 promise 畢竟屬於 FP 產物,最後都引入更像 imperative 的 async await
  • async await 只是讓你程式碼看起來很像 synchronous,但其本質仍然是 asynchronous,因為 async await 一定要搭配 promise,而 promise 就是 asynchronous,因此 async await 完全是 syntatic sugar
  • 雖然 async await 是 ECMAScript 2017 較新的東西,但個人認為 語意 其實並沒有 then() 好,它會讓你使用 imperative 方式思考,而 then() 語意會讓你使用 pipeline 思考,若搭配 Ramda 與 Wink-fp 之後,進而達成 function composition,保持 FP 原味,個人較喜歡 then()

Reference

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