ES2017 标准引入了 async
函数,使得异步操作变得更加方便。
async函数的定义方式
// 函数声明
async function fn() {}
// 函数表达式
const fn = async function () {}
// 箭头函数
const fn = async () => {}
// 对象的方法
let obj = { async fn() {} }
obj.fn().then(...)
核心用法
-
await
命令只能用在async
函数之中,如果用在普通函数中会报错。 -
async
函数体内,await
命令之前的代码以及await
命令所在语句中await
后的代码都为正常的同步代码,在async
函数调用时正常执行。 -
async
函数会返回一个Promise
对象。若返回的不是一个显式的Promise
对象,则会使用Promise.resolve()
包装成Promise
对象。 -
await
命令后跟一条返回Promise
对象的语句。若语句返回的不是Promise
对象,则会使用Promise.resolve()
包装成Promise
对象。 -
await
命令相当于其后语句返回的Promise
对象调用的只有一个回调函数的then
方法。async
函数体内await
命令所在语句后的语句相当于then
方法第一个回调函数中的语句,await
关键字左侧返回的值相当于要传入then
方法第一个回调函数的参数。 -
await
后语句返回的Promise
对象的执行结果可能是rejected
,所以最好把await
命令放在try...catch
代码块中。
示例代码:
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
代码块中。