ES2017 标准引入了 async 函数,使得异步操作变得更加方便。
async函数的定义方式
// 函数声明
async function fn() {}
// 函数表达式
const fn = async function () {}
// 箭头函数
const fn = async () => {}
// 对象的方法
let obj = { async fn() {} }
obj.fn().then(...)
核心用法
async函数会返回一个Promise对象。若返回的不是一个显示的Promise对象,则会使用Promise.resolve()包装成Promise对象。
await命令后跟一条返回Promise对象的语句。若语句返回的不是Promise对象,则会使用Promise.resolve()包装成Promise对象。
await命令相当于其后Promise对象调用的只有一个回调函数的then方法。async函数体内await命令所在语句后的语句相当于then方法第一个回调函数中的语句,await命令返回的值相当于传入then方法第一个回调函数的参数。
示例代码:
async function fn() {
return 100 // 相当于 return Promise.resolve(100)
}
console.log(fn()) // Promise {<fulfilled>: 100}
(async function () {
const a = await new Promise((resolve, reject) => {
console.log('start')
resolve('hello')
console.log('middle')
}).then((val) => {
console.log(val)
return 'world'
})
console.log(a)
})()
console.log('end')
// start
// middle
// end
// hello
// world
错误处理
因为async函数体内await命令所在语句后的语句相当于then方法第一个回调函数中的语句,所以如果任何一个await语句后面的 Promise 对象变为reject状态,那么整个async函数都会中断执行。
(async function () {
await Promise.reject('出错了')
await Promise.resolve('hello world'); // 不会执行
})()
console.log('hello') // 仍然可以执行
常见错误方式
1. await后的Promise变为reject状态
await命令后面的 Promise 对象如果变为rejected状态,则reject的参数会被catch方法的回调函数接收到。
(async function () {
await Promise.reject('出错了')
console.log('hello'); // 不会执行
})()
.catch(e => console.log('err~', e))
// err~ 出错了
注意,上面代码中,await语句前面没有return,但是reject方法的参数依然传入了catch方法的回调函数。这里如果在await前面加上return,效果是一样的。
2. await后的异步操作出错
若Promise内部直接抛出错误(如以下的throw new Error('出错了')),则Promise状态会变为rejected,reject的参数即为抛出的错误对象(如以下的Error('出错了'))
(async function () {
await new Promise(() => {
throw new Error('出错了')
console.log('hello') // 不会执行
})
})()
.catch(e => console.log('err~', e))
// err~ Error: 出错了
3. async函数内部抛出错误
async函数内部直接抛出错误,会导致async函数返回的 Promise 对象变为rejected状态。抛出的错误对象会被catch方法回调函数接收到。
(async function () {
throw new Error('出错了');
console.log('hello'); // 不会执行
})()
.catch(e => console.log('err~', e))
// err~ Error: 出错了
// 类似于如下
new Promise(() => {
throw new Error('出错了')
console.log('hello'); // 不会执行
})
.catch(e => console.log('err~', e))
错误处理方式
如果希望即使一个异步操作失败,也不要中断后面的异步操作,可以将可能出错的await放在try...catch结构里面,这样不管这个异步操作是否成功,后面的await都会执行。
async function f() {
try {
await Promise.reject('出错了')
} catch (e) {}
return await Promise.resolve('hello world')
}
f().then(v => console.log(v))
// hello world
如果await后跟的是Promise对象,则也可以后面的 Promise 对象再跟一个catch方法,处理前面可能出现的错误。
async function f() {
await Promise.reject('出错了')
.catch(e => console.log(e));
return await Promise.resolve('hello world');
}
f().then(v => console.log(v))
// 出错了
// hello world
如果有多个await命令,可以统一放在try...catch结构中。
async function main() {
try {
const val1 = await firstStep();
const val2 = await secondStep(val1);
const val3 = await thirdStep(val1, val2);
console.log('Final: ', val3);
} catch (err) {
console.error(err);
}
}
Promise对象的状态变化
async函数返回的 Promise 对象,必须等到async函数内部所有await命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return语句或者抛出错误。也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。
示例代码:
function getSquare(num) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(num ** 2)
}, 1000)
})
}
async function fn(num) {
const val1 = await getSquare(num)
const val2 = await getSquare(val1)
return val2
}
fn(3).then(console.log)
// 延迟两秒后,打印81
上面代码中,函数fn内部有两个操作:将输入平方、将得到的结果再次平方。只有这两个操作全部完成,才会执行then方法里面的console.log。
继发与并发
多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。
let foo = await getFoo();
let bar = await getBar();
上面代码中,getFoo和getBar是两个独立的异步操作(即互不依赖),被写成继发关系。这样比较耗时,因为只有getFoo完成以后,才会执行getBar,完全可以让它们同时触发。
如下写法,getFoo和getBar是同时触发,这样就会缩短程序的执行时间。
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
示例代码:
// 定义求平方函数
function getSquare(num) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(num ** 2)
}, 1000)
})
}
// 继发处理
!(async function () {
const arr = [1, 2, 3]
const res = []
for (const num of arr) {
res.push(await getSquare(num))
}
console.log(res)
})()
// 延时3秒后,打印[1, 4, 9]
// 并发处理
!(async function () {
const arr = [1, 2, 3]
const promiseArr = arr.map(x => getSquare(x))
let res = await Promise.all(promiseArr)
console.log(res)
})()
// 延时1秒后,打印[1, 4, 9]
遍历
数组的forEach()、map()等方法都不支持按顺序等待异步处理(即不支持上述继发处理),for、for...in、for...of 都会按顺序等待异步。
示例代码:
function getSquare(num) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(num ** 2)
}, 1000)
})
}
!(function () {
const arr = [1, 2, 3]
arr.forEach(async num => {
console.log(await getSquare(num));
})
})()
// 延时1秒后,同时打印1 4 9
!(async function () {
const arr = [1, 2, 3]
for (const num of arr) {
console.log(await getSquare(num));
}
})()
// 延时1秒打印1,再延时1秒打印4,再延时1秒打印9
注意事项
-
await命令只能用在async函数之中,如果用在普通函数中会报错。 -
async函数体内,await命令之前的代码以及await命令所在语句中await后的代码都为正常的同步代码,在async函数调用时正常执行。 -
await命令后的Promise对象的运行结果可能是rejected,所以最好把await命令放在try...catch代码块中。










