async function getResponseSize(url) {
const response = await fetch(url);
const reader = response.body.getReader();
let result = await reader.read();
let total = 0;
while (!result.done) {
const value = result.value;
total += value.length;
console.log('Received chunk', value);
// get the next result
result = await reader.read();
}
return total;
}
“高大上”的代码不见了,让人头疼的异步循环被替换成可靠却单调乏味的 while 循环, 但代码的可读性大大提高。
ES7 引入的 async/await 是 JavaScript 异步编程的一个重大改进,提供了在不阻塞主线程的情况下使用同步代码异步访问资源的能力。
Chrome 55 中默认情况下启用异步函数(Async functions),坦率地讲,这个特性相当实用。 可以利用它们像编写同步代码那样编写基于 Promise 的代码,而且还不会阻塞主线程,还能够大大提高代码的可读性。
Async/Await 起源
早在 2012 年微软的 C# 语言发布 5.0 版本时,就正式推出了 Async/Await 的概念,随后在 Python 和 Scala 中也相继出现了 Async/Await 的身影。再之后,才是我们今天讨论的主角,ES 2016 中正式提出了 Async/Await 规范。
其实在前端领域,也有不少类 Async/Await 的实现,其中不得不提到的就是知名网红之一的老赵写的 wind.js,站在今天的角度看,windjs 的设计和实现不可谓不超前。
Async/Await 实现
根据 Async/Await 的规范 中的描述 —— 一个 Async 函数总是会返回一个 Promise —— 不难看出 Async/Await 和 Promise 存在千丝万缕的联系。这也是为什么很多的同学都说,Async/Await 不过是一个语法糖。
单谈规范太枯燥,我们还是看看实际的代码。下面是一个最基础的 Async/Await 例子:
使用 Babel 转换后:
不难看出,Async/Await 的实现被转换成了基于 Promise 的调用。值得注意的是,原来只需 3 行代码即可解决的问题,居然被转换成了 52 行代码,这还是基于执行环境中已经存在 regenerator 的前提之一。如果要在兼容性尚不是非常理想的 Web 环境下使用,代码 overhead 的成本不得不纳入考虑。
返回值
无论是你否使用了 await,异步函数都会返回 Promise。该 Promise resolves 时返回异步函数返回的任何值,rejects 时返回异步函数抛出的任何值。
因此,对于:
…调用 hello() 返回的 Promise 会在 fulfills 时返回 "world"。
调用 foo() 返回的 Promise 会在 rejects 时返回 Error('bar')。
示例:流式传输响应
异步函数在更复杂示例中对比更加强烈。假设我们想在流式传输响应的同时记录数据块日志,并返回数据块最终大小。
以下是使用 Promise 编写的代码:
这段代码通过在 processResult 内递归调用来实现异步循环? 这样编写的代码可能让人觉看看起来“高大上”,但是实在是不太直观,谈不上简约优雅。
我们再用异步函数来改进上面这段代码:
“高大上”的代码不见了,让人头疼的异步循环被替换成可靠却单调乏味的 while 循环, 但代码的可读性大大提高。
避免太过串行化
虽然 await 可以让你的代码看起来像是同步的,但请记住,它们仍然是异步的,要避免太过串行化。
以上代码执行完毕需要 1000 毫秒,再看看这段代码:
以上代码只需 500 毫秒就可执行完毕,因为两个 wait 是同时发生的。
示例:按顺序输出获取的数据
假定我们想获取一系列网址,并尽快按正确顺序将它们记录到日志中。
以下是使用 Promise 编写的代码:
是的,没错,这里使用 reduce 来链接 Promise 序列。看起来很”高大上“,不过可读性嘛,见仁见智吧,我个人是不太喜欢这种需要 ”二次思考“ 的代码啦。
不过,如果使用异步函数改写以上代码,又容易让代码变得过于循序:
不推荐的编码方式 - 过于循序
代码简洁得多,但我的第二次获取要等到第一次获取读取完毕才能开始,以此类推。 其执行效率要比并行执行获取的 Promise 示例低得多。 幸运的是,还有一种理想的中庸之道:
推荐的编码方式 - 可读性强、并行效率高
在本例中,以并行方式获取和读取网址,但将的 reduce 部分替换成标准单调乏味但可读性强的 for 循环。
缺点
异常处理
正如在上文中提到的,async 函数默认会返回一个 Promise,这也意味着 Promise 中存在的问题 async 函数也会遇到,那就是 —— 默认会静默的吞掉异常。
所以,虽然 Async/Await 能够使用 try...catch... 这种符合同步习惯的方式进行异常捕获,你依然不得不手动给每个 await 调用添加 try...catch... 语句,否则,async 函数返回的只是一个 reject 掉的 Promise 而已。
控制流
虽然处理异步问题的技术一直在进步,但是在实际工程实践中,我们对异步操作的需求也在不断扩展加深,这也是为什么各种 flow control 的库一直兴盛不衰的原因之一。
Async/Await 在处理异步问题的有一定的优越性,但也存在一些不足:
当然,站在 EMCA 规范的角度来看,有些需求可能比较少见,但是如果纳入规范中,也可以减少前端程序员在挑选异步流程控制库时的纠结了。
参考