Async Function
Async Function,即异步函数,为异步编程带来了非常大的提升,瞬间把开发效率和维护效率提升了一个数量级。
它的用法非常简单,只要用 async
关键字声明一个函数为“异步函数”,这个函数的内部就可以使用 await 关键字,让其中的语句等待异步执行的结果,然后再继续执行。我们还是用代码来说话吧:
function resolveAfter2Seconds(x) {
return new Promise(resolve => {
setTimeout(() => {
resolve(x);
}, 2000);
});
}
async function f1() {
var x = await resolveAfter2Seconds(10);
console.log(x);
}
f1();
// 输出:
// (2秒后)10
这段代码来自 MDN。里面先声明了 resolveAfter2Seconds
函数,执行它会返回一个 Promise 对象,等待2秒钟之后完成。然后声明了异步函数 f1
,里面只有两行代码,第一行用 await
表示要等后面的 Promise 完成,再进行下一步;于是2秒之后,输出了“10”。
这段代码比较短,不太能体现异步函数的价值,我们改写一下查找最大文件的代码试试看:
const fs = require('./FileSystem');
async function findLargest(dir) {
let files = await fs.readdir(dir);
let stats = [];
for (let i = 0, len = files.length; i < len; i++) {
stats.push(await fs.stat(files[i]));
}
return stats
.filter( stat => stat.isFile() )
.reduce( (memo, stat) => {
if (memo.size > stat.size) return memo;
return stat;
});
}
findLargest('path/to/dir')
.then(file => {
console.log(file);
});
怎么样,是不是异常清晰明了,连代码量都减少了很多。而且,因为每次 await
都会等待后面的异步函数完成,所以我们还无意间写出了生成队列的代码。当然你可能不希望要队列,毕竟都异步了,队列的吸引力实在不大,那么我们只需要把 for
循环改成下面这个样子就行了:
let stats = await Promise.all(files.map( file => fs.stat(file) ));
酷不酷!想不想学?
设计
见识过异步函数的强大之后,我们来捋一捋一下它的具体设计。
异步函数主要由两部分组成:
async
声明一个函数是异步函数。执行异步函数会返回一个 Promise 实例。异步函数的返回值会继续向后传递。
async
的语法比较简单,就在普通函数前面加个 async
而已。它也支持箭头函数,也可以用作立刻执行函数。
await
await
操作符只能用在异步函数的内部。表示等待一个 Promise 完成,然后返回其返回值。语法是这样的:
[return_value] = await expression;
- return_value 返回值:Promise 执行器的返回值
- expression 表达式:Promise 对象,其它类型会使用
Promise.resolve()
转换
执行到 await
之后,异步函数会暂停执行,等待表达式里的 Promise 完成。resolve
之后,返回 Promise 执行器的返回值,然后继续执行。如果 Promise 被 rejected
,就抛出异常。
捕获错误
异步函数最大的改进其实就在捕获错误这里。它可以直接使用 try/catch/throw
就像我们之前那样。
比如,一款前后端分离的应用,论坛,需要用户登录。所以在初始化的时候就会向服务器请求用户信息,如果返回了,就继续加载用户关注列表等其它数据;如果返回 401 错误,就让用户登录。用 Promise 的话,我们可能会这样做:
function getMyProfile() {
return fetch('api/me')
.then(response => {
if (response.ok) {
return response.json();
}
})
.then(profile => {
global.profile = profile
})
.catch( err => {
if (err.statusCode === 401) {
location.href = '#/login';
} else {
// 处理其它错误
}
});
}
这样做也不是不可以,但是如果业务逻辑越来越复杂,队列也会越来越长,也许每一步都需要处理错误,那么队列就会变得难以维护。
如果用异步函数就会简单很多:
async function getMyProfile() {
let profile;
try {
global.profile = await fetch('api/me');
} catch (e) {
if (e.statusCode === 401) {
location.href = '#/login';
} else {
// 处理其它错误
}
}
}
另外,如果你真的捕获到错误,你会发现,它的堆栈信息是连续的,甚至可以回溯到调用异步函数的语句,继而可以审查所有堆栈里的变量。这对调试程序带来的帮助非常巨大。(需要运行时支持,转译的不行。)
普及率
可以看出,大部分主流浏览器都已经支持异步函数,值得关注的,只有 iOS 10.2 和 Android 4。当然在国内的话,IE 始终是老大难问题……