本期精读的文章是:6 Reasons Why JavaScript’s Async/Await Blows Promises Away
我为什么要选这篇文章呢?
前端异步问题处理一直是一个老大难的问题,前有 Callback Hell 的绝望,后有 Promise/Deferred 的规范混战,从 Generator 配合 co 所向披靡,到如今 Async/Await 改变世界。为什么异步问题如此难处理,Async/Await 又能在多大程度上解决我们开发和调试过程中遇到的难点呢?希望这篇文章能给我们带来一些启发。
当然,本文不是一篇针对前端异步问题综合概要性的文章,更多的是从 Async/Await 的优越性谈起。但这并不妨碍我们从 Async/Await 的特点出发,结合自己在工作、开发过程中的经验教训,认真的思考和总结如何更优雅、更高效的处理异步问题。
Async/Await 的优点:
- 语法简洁清晰,节省了很多不必要的匿名函数
- 直接使用 try...catch... 进行异常处理
- 添加条件判断更符合直觉
- 减少不必要的中间变量
- 更清晰明确的错误堆栈
- 调试时可以轻松给每个异步调用加断点
Async/Await 的局限:
- 降低了我们阅读理解代码的速度,此前看到
.then()
就知道是异步,现在需要识别async
和await
关键字 - 目前支持 Async/Await 的 Node.js 版本(Node 7)并非 LTS 版本,但是下一个 LTS 版本很快将会发布
可以看出,文中提到 Async/Await 的优势大部分都是从开发调试效率提升层面来讲的,提到的问题或者说局限也只有不痛不痒的两点。
让我们来看看参与精读的同学都提出了哪些深度观点:
本次提出独到观点的同学有:@javie007 @流形 @camsong @Turbe Xue @淡苍 @留影 @黄子毅 精读由此归纳。
参与精读的很多同学都提出来,Async/Await 并不是什么新鲜的概念,事实的确如此。
早在 2012 年微软的 C# 语言发布 5.0 版本时,就正式推出了 Async/Await 的概念,随后在 Python 和 Scala 中也相继出现了 Async/Await 的身影。再之后,才是我们今天讨论的主角,ES 2016 中正式提出了 Async/Await 规范。
以下是一个在 C# 中使用 Async/Await 的示例代码:
public async Task<int> SumPageSizesAsync(IList<Uri> 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 中的使用方法:
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,站在今天的角度看,windjs 的设计和实现不可谓不超前。
根据 Async/Await 的规范 中的描述 —— 一个 Async 函数总是会返回一个 Promise —— 不难看出 Async/Await 和 Promise 存在千丝万缕的联系。这也是为什么不少参与精读的同学都说,Async/Await 不过是一个语法糖。
单谈规范太枯燥,我们还是看看实际的代码。下面是一个最基础的 Async/Await 例子:
async function test() {
const img = await fetch('tiger.jpg');
}
使用 Babel 转换后:
'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 的出现表现出多么大的兴趣,几种常见的观点是:「还不是基于 Promise 的语法糖,没什么意思」、「现在使用 co 已经能完美解决异步问题,不需要再引入什么新的概念」、「浏览器兼容性这么差,用 Babel 编译又需要引入不少依赖,使用成本太高」等等。
在本次精读中,也有不少同学指出了使用 Async/Await 的局限性。
比如,使用 Async/Await 并不能很好的支持异步并发。考虑下面这种情况,一个模块需要发送 3 个请求并在获得结果后才能进行渲染,3 个请求之间没有依赖关系。如果使用 Async/Await,写法如下:
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
封装一层:
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 规范的角度来看,有些需求可能比较少见,但是如果纳入规范中,也可以减少前端程序员在挑选异步流程控制库时的纠结了。
Async/Await 的确是更优越的异步处理方案,但我们相信这一定不是终极处理方案。随着前端工程化的深入,一定有更多、更复杂、更精细的异步问题出现,同时也会有迎合这些问题的解决方案出现,比如精读中很多同学提到的 RxJS 和 js-csp。
如果你想参与讨论,请点击这里,每周都有新的主题,每周五发布。