blog
blog copied to clipboard
async/await 小结
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 例子:
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 的成本不得不纳入考虑。
返回值
无论是你否使用了 await,异步函数都会返回 Promise。该 Promise resolves 时返回异步函数返回的任何值,rejects 时返回异步函数抛出的任何值。
因此,对于:
// wait ms milliseconds
function wait(ms) {
return new Promise(r => setTimeout(r, ms));
}
async function hello() {
await wait(500);
return 'world';
}
…调用 hello() 返回的 Promise 会在 fulfills 时返回 "world"。
async function foo() {
await wait(500);
throw Error('bar');
}
调用 foo() 返回的 Promise 会在 rejects 时返回 Error('bar')。
示例:流式传输响应
异步函数在更复杂示例中对比更加强烈。假设我们想在流式传输响应的同时记录数据块日志,并返回数据块最终大小。
以下是使用 Promise 编写的代码:
function getResponseSize(url) {
return fetch(url).then(response => {
const reader = response.body.getReader();
let total = 0;
return reader.read().then(function processResult(result) {
if (result.done) return total;
const value = result.value;
total += value.length;
console.log('Received chunk', value);
return reader.read().then(processResult);
})
});
}
这段代码通过在 processResult 内递归调用来实现异步循环? 这样编写的代码可能让人觉看看起来“高大上”,但是实在是不太直观,谈不上简约优雅。
我们再用异步函数来改进上面这段代码:
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 循环, 但代码的可读性大大提高。
避免太过串行化
虽然 await 可以让你的代码看起来像是同步的,但请记住,它们仍然是异步的,要避免太过串行化。
async function series() {
await wait(500);
await wait(500);
return "done!";
}
以上代码执行完毕需要 1000 毫秒,再看看这段代码:
async function parallel() {
const wait1 = wait(500);
const wait2 = wait(500);
await wait1;
await wait2;
return "done!";
}
以上代码只需 500 毫秒就可执行完毕,因为两个 wait 是同时发生的。
示例:按顺序输出获取的数据
假定我们想获取一系列网址,并尽快按正确顺序将它们记录到日志中。
以下是使用 Promise 编写的代码:
function logInOrder(urls) {
// fetch all the URLs
const textPromises = urls.map(url => {
return fetch(url).then(response => response.text());
});
// log them in order
textPromises.reduce((chain, textPromise) => {
return chain.then(() => textPromise)
.then(text => console.log(text));
}, Promise.resolve());
}
是的,没错,这里使用 reduce 来链接 Promise 序列。看起来很”高大上“,不过可读性嘛,见仁见智吧,我个人是不太喜欢这种需要 ”二次思考“ 的代码啦。
不过,如果使用异步函数改写以上代码,又容易让代码变得过于循序:
不推荐的编码方式 - 过于循序
async function logInOrder(urls) {
for (const url of urls) {
const response = await fetch(url);
console.log(await response.text());
}
}
代码简洁得多,但我的第二次获取要等到第一次获取读取完毕才能开始,以此类推。 其执行效率要比并行执行获取的 Promise 示例低得多。 幸运的是,还有一种理想的中庸之道:
推荐的编码方式 - 可读性强、并行效率高
async function logInOrder(urls) {
// fetch all the URLs in parallel
const textPromises = urls.map(async url => {
const response = await fetch(url);
return response.text();
});
// log them in sequence
for (const textPromise of textPromises) {
console.log(await textPromise);
}
}
在本例中,以并行方式获取和读取网址,但将的 reduce 部分替换成标准单调乏味但可读性强的 for 循环。
缺点
异常处理
正如在上文中提到的,async 函数默认会返回一个 Promise,这也意味着 Promise 中存在的问题 async 函数也会遇到,那就是 —— 默认会静默的吞掉异常。
所以,虽然 Async/Await 能够使用 try...catch... 这种符合同步习惯的方式进行异常捕获,你依然不得不手动给每个 await 调用添加 try...catch... 语句,否则,async 函数返回的只是一个 reject 掉的 Promise 而已。
控制流
虽然处理异步问题的技术一直在进步,但是在实际工程实践中,我们对异步操作的需求也在不断扩展加深,这也是为什么各种 flow control 的库一直兴盛不衰的原因之一。
Async/Await 在处理异步问题的有一定的优越性,但也存在一些不足:
- 缺少复杂的控制流程,如 always、progress、pause、resume 等
- 缺少中断的方法,无法 abort
当然,站在 EMCA 规范的角度来看,有些需求可能比较少见,但是如果纳入规范中,也可以减少前端程序员在挑选异步流程控制库时的纠结了。
参考
good