本期精读的文章是:[6 Reasons Why JavaScript’s Async/Await Blows Promises Away](https://hackernoon.com/6-reasons-why-javascripts-async-await-blows-promises-away-tutorial-c7ec10518dd9) # 1 引言 logo 我为什么要选这篇文章呢? 前端异步问题处理一直是一个老大难的问题,前有 Callback Hell 的绝望,后有 Promise/Deferred 的规范混战,从 Generator 配合 co 所向披靡,到如今 Async/Await 改变世界。为什么异步问题如此难处理,Async/Await 又能在多大程度上解决我们开发和调试过程中遇到的难点呢?希望这篇文章能给我们带来一些启发。 当然,本文不是一篇针对前端异步问题综合概要性的文章,更多的是从 Async/Await 的优越性谈起。但这并不妨碍我们从 Async/Await 的特点出发,结合自己在工作、开发过程中的经验教训,认真的思考和总结如何更优雅、更高效的处理异步问题。 # 2 内容概要 Async/Await 的优点: - 语法简洁清晰,节省了很多不必要的匿名函数 - 直接使用 try...catch... 进行异常处理 - 添加条件判断更符合直觉 - 减少不必要的中间变量 - 更清晰明确的错误堆栈 - 调试时可以轻松给每个异步调用加断点 Async/Await 的局限: - 降低了我们阅读理解代码的速度,此前看到 `.then()` 就知道是异步,现在需要识别 `async` 和 `await` 关键字 - 目前支持 Async/Await 的 Node.js 版本(Node 7)并非 LTS 版本,但是下一个 LTS 版本很快将会发布 可以看出,文中提到 Async/Await 的优势大部分都是从开发调试效率提升层面来讲的,提到的问题或者说局限也只有不痛不痒的两点。 让我们来看看参与精读的同学都提出了哪些深度观点: # 3 精读 本次提出独到观点的同学有:[@javie007](http://link.zhihu.com/?target=https%3A//github.com/javie007) [@流形](https://www.zhihu.com/people/6c772f9726a914ed4a4b90c88010461c) [@camsong](https://www.zhihu.com/people/078cc0fb15845759ad8295b0f0e50099) [@Turbe Xue](https://www.zhihu.com/people/turbe-xue) [@淡苍](https://www.zhihu.com/people/5ac53c9c0484e83672e1c1716bdf0ff9) [@留影](https://www.zhihu.com/people/38c3c75795824de1bc5d99cff904a832) [@黄子毅](https://www.zhihu.com/people/3ec85a04bc9eaa35b1830874cc463a52) 精读由此归纳。 ### Async/Await 并不是什么新鲜概念 参与精读的很多同学都提出来,Async/Await 并不是什么新鲜的概念,事实的确如此。 早在 2012 年微软的 C# 语言发布 5.0 版本时,就正式推出了 Async/Await 的概念,随后在 Python 和 Scala 中也相继出现了 Async/Await 的身影。再之后,才是我们今天讨论的主角,ES 2016 中正式提出了 Async/Await 规范。 以下是一个在 C# 中使用 Async/Await 的示例代码: ```c-sharp public async Task SumPageSizesAsync(IList uris) { int total = 0; foreach (var uri in uris) { statusText.Text = string.Format("Found {0} bytes ...", total); var data = await new WebClient().DownloadDataTaskAsync(uri); total += data.Length; } statusText.Text = string.Format("Found {0} bytes total", total); return total; } ``` 再看看在 JavaScript 中的使用方法: ```javascript async function createNewDoc() { let response = await db.post({}); // post a new doc return await db.get(response.id); // find by id } ``` 不难看出两者单纯在异步语法上,并没有太多的差异。这也是为什么 Async/Await 推出后,获得不少赞许和亲切感的原因之一吧。 其实在前端领域,也有不少类 Async/Await 的实现,其中不得不提到的就是知名网红之一的老赵写的 [wind.js](https://github.com/JeffreyZhao/wind),站在今天的角度看,windjs 的设计和实现不可谓不超前。 ### Async/Await 是如何实现的 根据 [Async/Await 的规范](https://tc39.github.io/ecmascript-asyncawait/) 中的描述 —— 一个 Async 函数总是会返回一个 Promise —— 不难看出 Async/Await 和 Promise 存在千丝万缕的联系。这也是为什么不少参与精读的同学都说,Async/Await 不过是一个语法糖。 单谈规范太枯燥,我们还是看看实际的代码。下面是一个最基础的 Async/Await 例子: ```javascript async function test() { const img = await fetch('tiger.jpg'); } ``` 使用 Babel 转换后: ```javascript 'use strict'; var test = function() { var _ref = _asyncToGenerator(regeneratorRuntime.mark(function _callee() { var img; return regeneratorRuntime.wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: _context.next = 2; return fetch('tiger.jpg'); case 2: img = _context.sent; case 3: case 'end': return _context.stop(); } } }, _callee, this); })); return function test() { return _ref.apply(this, arguments); }; }(); function _asyncToGenerator(fn) { return function() { var gen = fn.apply(this, arguments); return new Promise(function(resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function(value) { step("next", value); }, function(err) { step("throw", err); }); } } return step("next"); }); }; } ``` 不难看出,Async/Await 的实现被转换成了基于 Promise 的调用。值得注意的是,原来只需 3 行代码即可解决的问题,居然被转换成了 52 行代码,这还是基于执行环境中已经存在 regenerator 的前提之一。如果要在兼容性尚不是非常理想的 Web 环境下使用,代码 overhead 的成本不得不纳入考虑。 ### Async/Await 真的是更优秀的替代方案吗 不知道是个人观察偏差,还是大家普遍都有这样的看法。在国内前端圈子里,并没有对 Async/Await 的出现表现出多么大的兴趣,几种常见的观点是:「还不是基于 Promise 的语法糖,没什么意思」、「现在使用 co 已经能完美解决异步问题,不需要再引入什么新的概念」、「浏览器兼容性这么差,用 Babel 编译又需要引入不少依赖,使用成本太高」等等。 在本次精读中,也有不少同学指出了使用 Async/Await 的局限性。 比如,使用 Async/Await 并不能很好的支持异步并发。考虑下面这种情况,一个模块需要发送 3 个请求并在获得结果后才能进行渲染,3 个请求之间没有依赖关系。如果使用 Async/Await,写法如下: ```javascript async function mount() { const result1 = await fetch('a.json'); const result2 = await fetch('b.json'); const result3 = await fetch('c.json'); render(result1, result2, result3); } ``` 这样的写法在异步上确实简洁不少,但是 3 个异步请求是顺序执行的,并没有充分利用到异步的优势。要想实现真正的异步,还是需要依赖 `Promise.all` 封装一层: ```javascript async function mount() { const result = await Promise.all([ fetch('a.json'), fetch('b.json'), fetch('c.json') ]); render(...result); } ``` 此外,正如在上文中提到的,async 函数默认会返回一个 Promise,这也意味着 Promise 中存在的问题 async 函数也会遇到,那就是 —— 默认会静默的吞掉异常。 所以,虽然 Async/Await 能够使用 try...catch... 这种符合同步习惯的方式进行异常捕获,你依然不得不手动给每个 await 调用添加 try...catch... 语句,否则,async 函数返回的只是一个 reject 掉的 Promise 而已。 ### 异步还有哪些问题需要解决 虽然处理异步问题的技术一直在进步,但是在实际工程实践中,我们对异步操作的需求也在不断扩展加深,这也是为什么各种 flow control 的库一直兴盛不衰的原因之一。 在本次精读中,大家肯定了 Async/Await 在处理异步问题的优越性,但也提到了其在异步问题处理上的一些不足: - 缺少复杂的控制流程,如 always、progress、pause、resume 等 - 缺少中断的方法,无法 abort 当然,站在 EMCA 规范的角度来看,有些需求可能比较少见,但是如果纳入规范中,也可以减少前端程序员在挑选异步流程控制库时的纠结了。 # 3 总结 Async/Await 的确是更优越的异步处理方案,但我们相信这一定不是终极处理方案。随着前端工程化的深入,一定有更多、更复杂、更精细的异步问题出现,同时也会有迎合这些问题的解决方案出现,比如精读中很多同学提到的 RxJS 和 js-csp。 > 讨论地址是:[那些年我们处理过的异步问题 · Issue #6 · dt-fe/weekly](http://link.zhihu.com/?target=https%3A//github.com/dt-fe/weekly/issues/6) > 如果你想参与讨论,请[点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,每周五发布。