内容简介:CertSimple 网站最近发布了一篇文章,说 ES2017 里的 async 和 await 是 JS 最好的特性。我非常赞同。基本上来说,JS 为数不多的几个优点之一就是对异步请求的处理得当。这得益于它从 Scheme 那里继承来的函数和闭包。
CertSimple 网站最近发布了一篇文章,说 ES2017 里的 async 和 await 是 JS 最好的特性。我非常赞同。
基本上来说,JS 为数不多的几个优点之一就是对异步请求的处理得当。这得益于它从 Scheme 那里继承来的函数和闭包。
然而这也是 JS 的最大的问题之一,因为这导致了回调地狱(callback hell),这个看起来无法回避的问题导致异步的 JS 代码可读性非常差。为了解决回调地狱,大家尝试了很多方案,但大都失败了。Promise 方案差点解决了这个问题,但还是失败了。
最终,我们看到了 async/await 与 Promise 联合的方案,这个方案非常好地解决了问题。在这篇文章里,我将解释为什么会这样,以及 Promise、async/await 和 do 语法、monad 之间的关系。
首先,我们尝试用三种不同风格的代码来获取读取用户所有账户里的余额。(一个用户有多个账户 accout,每个账户里都有余额 balence)
错误的方案:回调地狱
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 个问题需要解决:
- 每一个地方都要对 err 进行了处理
- 用计数器来计算异步得来的值
- 不可避免的嵌套
几乎正确的方案:Promise
function getBalances() { return api.getAccounts() .then(accounts => Promise.all(accounts.map(api.getBalance)) .then(balances => Ramda.zipObject(accounts, balances)) ); } 复制代码
这个代码解决了上面的三个问题:
- 我们可以在最后一个 then 里统一处理 error
- Promise.all 使得我们不需要定义额外的计数器
- 我们可以最大程度地避免嵌套
但是还有一个问题没有解决,那就是 then 还是嵌套了,第二个 then 在第一个 then 的回调里,因为第二个 then 需要用到第一个then 的 accounts 变量。所以对代码进行正确的缩进非常重要。
不过解决方法也是有的,那就是让第一个 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 个踩。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 译文 | 推荐信:程序排错
- Protobuf -java基础教程(译文)
- 译文: Basics of Futexes
- 跨站请求伪造已经死了!(译文)
- (译文)通过一个例子理解paxos算法
- iOS·UIView Apple 官方文档译文
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
拆掉互联网那堵墙
庄良基 / 经济日报出版社 / 2014-6 / 25.80
都在说道互联网、说道电子商务、说道移动APP、说道微信、说道互联网金融......我们该如何认识互联网?中小微企业该如何借力互联网?互联网很神秘吗?很高深莫测吗? 其实互联网并没有什么神秘的,也没有什么高深莫测的!互联网无非是人类发明的工具而已,既然是工具,我们就一定可以驾驭和使用它。既然可以双重使用,就理当让所有有人都容易掌握并轻松驾驭。 互联网离我们很远吗?互联网界的成功故事都是那......一起来看看 《拆掉互联网那堵墙》 这本书的介绍吧!