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 个踩。

相关文章
相关标签/搜索