async/await 之於 Promise,正如 do 之於 monad(譯文)

原文連接javascript

CertSimple 網站最近發佈了一篇文章,說 ES2017 裏的 async 和 await 是 JS 最好的特性。我很是贊同。java

基本上來講,JS 爲數很少的幾個優勢之一就是對異步請求的處理得當。這得益於它從 Scheme 那裏繼承來的函數和閉包。git

然而這也是 JS 的最大的問題之一,由於這致使了回調地獄(callback hell),這個看起來沒法迴避的問題致使異步的 JS 代碼可讀性很是差。爲了解決回調地獄,你們嘗試了不少方案,但大都失敗了。Promise 方案差點解決了這個問題,但仍是失敗了。github

最終,咱們看到了 async/await 與 Promise 聯合的方案,這個方案很是好地解決了問題。在這篇文章裏,我將解釋爲何會這樣,以及 Promise、async/await 和 do 語法、monad 之間的關係。編程

首先,咱們嘗試用三種不一樣風格的代碼來獲取讀取用戶全部帳戶裏的餘額。(一個用戶有多個帳戶 accout,每一個帳戶裏都有餘額 balence)api

錯誤的方案:回調地獄

function getBalances(callback) { 
  api.getAccounts(function (err, accounts) { // 回調
    if (err) {
      callback(err);
    } else {
      var balances = {}; // 餘額
      var balancesCount = 0; 
      accounts.forEach(function(account, i) {
        api.getBalance(function (err, balance) { // 回調
          if (err) {
            callback(err);
          } else {
            balances[account] = balance;
            if (++balancesCount === accounts.length) {
              callback(null, balances);
            }
          }
        });
      });
    }
  });
};
複製代碼

這是一種很容易想到的方法,可是它有兩層回調,這份代碼醜陋中有 3 個問題須要解決:promise

  1. 每個地方都要對 err 進行了處理
  2. 用計數器來計算異步得來的值
  3. 不可避免的嵌套

幾乎正確的方案:Promise

function getBalances() {
  return api.getAccounts()
    .then(accounts => 
        Promise.all(accounts.map(api.getBalance))
            .then(balances => Ramda.zipObject(accounts, balances))
    );
}
複製代碼

這個代碼解決了上面的三個問題:閉包

  1. 咱們能夠在最後一個 then 裏統一處理 error
  2. Promise.all 使得咱們不須要定義額外的計數器
  3. 咱們能夠最大程度地避免嵌套

可是還有一個問題沒有解決,那就是 then 仍是嵌套了,第二個 then 在第一個 then 的回調裏,由於第二個 then 須要用到第一個then 的 accounts 變量。因此對代碼進行正確的縮進很是重要。dom

不過解決方法也是有的,那就是讓第一個 then 把 accounts 傳給第二個 then:異步

function getBalances() {
  return api.getAccounts()
    .then(accounts => Promise.all(accounts.map(api.getBalance)
                                       .then(balances => [accounts, balances])))
    .then(([accounts, balances]) => Ramda.zipObject(accounts, balances));
}
複製代碼

可是這樣會致使又多了一個 then。能夠看到 Promise 基本上解決了回調低於,可是並無徹底解決。

正確的方案:async/await

async function getBalances() {
  const accounts = await api.getAccounts();
  const balances = await Promise.all(accounts.map(api.getBalance));
  return Ramda.zipObject(balances, accounts);
}
複製代碼

async 函數裏能夠出現 await 關鍵字,await 會獲得 Promise 對象完成任務,而後再執行下一句話。

有了這些咱們就不用再蛋疼地縮進了。這是如何作到的呢?咱們須要追根溯源。

回調地獄的起源

不少人都認爲回調地獄只有在異步任務中才有,實際上只要咱們用回調來處理被包裹的值,就會出現回調地獄。

假設你想打印出 [1,2,3] [4,5,6] [7,8,9] 的全部排列組合,好比 [1,4,7] [1,4,8] 等等:

[1,2,3].map((x) => {
  [4,5,6].map((y) => {
    [7,8,9].map((z) => { 
      console.log(x,y,z);
    })
  })
});
複製代碼

看,咱們熟悉的回調地獄出現了。這是徹底同步的代碼,可是 async 和 await 只能處理異步……

假設咱們爲同步代碼也建立相似的關鍵字叫作 multi/pick,那麼上面的代碼就能夠寫成

multi function () {
  x = pick [1, 2, 3];
  y = pick [4, 5, 6];
  z = pick [7, 8, 9];
  console.log(x, y, z);
}
複製代碼

固然,這個語法是不存在的。

Monad 和 do

有些語言擁有一些特性能處理全部的這類需求,而且不區分異步仍是同步。

譯註:中間的過程須要一些 TS 和 Haskell 知識,能看懂的請自行閱讀。代碼是大概是這樣的:

getBalances :: Promise (Map String String) -- 這是類型聲明
getBalances = do 
  accounts <- getAccounts
  balances <- getBalance accounts
  return (Map.fromList (zip accounts balances))
複製代碼

這個語法叫作 do 標記或者 do 語法。它要求 Promise 知足 Monad 的一些規則。

do 語法和 Monad 是在 1995 年被用在 Haskell 裏的(譯註:JS 在 2015 年,也就是 20 年後才把 Promise 引入)。

這兩個特性今後解決了回調地獄。若是把 JS 的 Promise、await/async 與 Haskell 的 Monad、do 語法作對比的話,你會發現

await/async 之於 Promise,正如 do 語法之於 Monad

既然 Haskell 上已經驗證了 Monad 可以有效避免回調地獄,那麼 JS 就能夠直接放心用 await 了。

總結

回調地獄沒了,JS is great again。可是爲何花了這麼久時間 JS 纔去借鑑 Monad 呢?要是 2013 年,社區裏的人遵從了『那個瘋狂的傢伙』的建議 就行了。

全文完。

譯註:那個瘋狂的傢伙說了什麼呢?打開連接你能夠看到一個 GitHub Issues 頁面,那個傢伙的名字叫作 Brian Mckenna(布萊恩)。

布萊恩提議使用函數式編程的方案來優化 Promise。

然而提案的維護者 domenic 卻並不領情。

domenic 說

咱們不會這樣作的。這種方案不切實際,爲了知足某些人本身的審美偏好創造出了奇怪而又無用的 API,沒法應用在 JS 裏。你沒有理解 Promise 要解決的問題是在命令式編程語言裏提供異步流程控制模型。 這種方案是很是不嚴密的(hilariously inaccurate),由於沒有知足咱們的 spec,應該只能經過咱們 1/500 的測試用例。

這個回覆獲得了 16 贊和 254 個踩。

相關文章
相關標籤/搜索