async/await 是什么
了解和使用 async
之前,需要提前了解以下部分:
- Event loop
- Promise
- Generator
async/await
是 ES7
专门为异步编程设计的语法,本质上是 Generator
的语法糖
在继承了 Generator
可分段执行程序的能力之外,弥补了 Generator
本身的不足
// async/await 之前
// 主要还是使用 Promise 链式调用的方式,形式上还是“链式调用” + “回调函数”
function task() {
task1()
.then(() => task2)
.then(() => task3)
}
// async/await 之后
// 真正同时做到了“异步任务按序执行”的“顺序执行写法”
async function task() {
await task1()
await task2()
...
}
基本执行流程:
-
async
函数task
执行到第一个await
异步任务task1
,会将执行线程让给task1
- 直到
task1
正常返回,才会重新获得执行线程,继续执行第二个await
异步任务task2
- 依次类推
重点主要分为两个部分:async
关键字和 await
关键字
async 关键字
async
类似 Generator
中的 *
,用于将函数定义为 async
函数
成为 AsyncFunction
构造函数的实例
AsyncFunction 参考
async
函数具备几个特点
- 语义清晰明确
- 返回 Promise 对象
- 自带执行器,开箱即用
- 可以被 try catch 捕获代码错误
语义清晰明确
相比于 *
和 yield
,async
异步,await
等待,清晰明确,没有歧义
返回 Promise 对象
返回值是 Promise
类型的对象,方便使用 Promise API
便于各类异步场景的运用
- 如果
async
函数返回的不是一个Promise
对象,会使用Promise.resolve()
进行处理返回 - 如果
async
函数返回一个Promise
对象,以这个对象为准 - 如果
async
函数报错或者reject
,会返回一个reject
的Promise
对象
自带执行器,开箱即用
不同于 Generator
函数需要使用 co
之类的函数库封装执行器
async
开箱即用,不需要额外的封装
可以被 try catch 捕获代码错误
之前的异步编程方案,例如 Promise
在异步任务中的代码错误,无法被 try catch
捕获
但是在 async
函数中,代码执行错误可以被 try catch
捕获了
// Promise
function promiseFun() {
try {
errFun()
} catch(err) {
console.log('err', err) // 不会执行
}
}
// async
async function asyncFun() {
try {
await errFun()
} catch(err) {
console.log('err', err) // 会执行
}
}
// 模拟异步代码错误
async function errFun() {
return Promise.resolve().then(() => {
'123'.filter(item => item.name) // 这里代码执行错误
})
}
await 关键字
await
类似 Generator
中的 await
,用于移交执行线程给其他函数
await 只能在 async 函数中使用
await
只能在 async
函数中使用,不能在其他类型函数中使用
否则会报错!
await 顺序执行会被 rejected 的 Promise 阻断
如下所示:
await
对应的表达式或者函数返回值,如果是一个 rejected
状态 的 Promise
async
函数会被中断执行
async function test(){
await Promise.resolve(1);
await Promise.reject(2);
await Promise.resolve(3); // 程序无法执行
return 'done';
}
因此,async/await
的一个重头戏就是错误处理
async/await 在使用中的问题
async/await
在使用中的问题基本有两个
- 错误处理
- 在循环迭代中的使用
async 错误处理
async
函数错误的基本处理方式有 try catch
和 Promise catch
两种方式
// try catch
async function fun1() {
try {
await somethingThatReturnsAPromise()
} catch(err) {
console.log(err)
}
}
// Promise catch
async function fun2() {
await somethingThatReturnsAPromise().catch((err) => {
console.log(err);
});
}
基于以上的两种方式,延伸出两种对应的优化方案
-
try catch
优化- 使用
loader
统一对 await 语句进行try catch
转换,实现开发环境不写try catch
,生产环境打包时自动生成try catch
代码 - 参考 嘿,不要给 async 函数写那么多 try/catch 了
- 使用
-
Promise catch
优化- 使用高阶函数封装
async
函数返回的Promise
对象的then
catch
函数 - 参考 async/await 优雅的错误处理方法
- 使用高阶函数封装
循环迭代中使用 async
我们平时常用的循环迭代方法,在与 async/await
结合使用时,会出现一些意料之外的情况
- work:按时间间隔执行打印
- no work:同时打印
下面是各个方案的结果:
- for 循环:work
- for in:work
- for of:work
- while:work
- forEach:no work
- map:no work
- filter:no work
- reduce:no work
// 工具函数
// 期望通过循环 list 执行 setTimeoutFun
// 实现每隔 1000ms 打印日志
const list = [1000, 1000, 1000]
const setTimeoutFun = (num) => {
return new Promise((resolve) => {
setTimeout(() => {
console.log(num, new Date())
resolve(num)
}, num)
})
}
// for 循环
async function forFun() {
for (let i = 0; i < list.length; i++) {
await setTimeoutFun(list[i])
}
}
// for in
async function forInFun() {
for (const i in list) {
await setTimeoutFun(list[i])
}
}
// for of
async function forOfFun() {
for (const num of list) {
await setTimeoutFun(num)
}
}
// while
async function whileFun() {
let i = 0
while(i <= list.length - 1) {
await setTimeoutFun(list[i])
i += 1
}
}
// forEach
function forEachFun() {
list.forEach(async (num) => {
await setTimeoutFun(num)
})
}
// map
function mapFun() {
list.map(async (num) => {
await setTimeoutFun(num)
})
}
// filter
function filterFun() {
list.filter(async (num) => {
await setTimeoutFun(num)
})
}
// reduce
function reduceFun() {
list.reduce(async (pre, next, index) => {
return await setTimeoutFun(next)
}, Promise.resolve())
}
forEach
、map
、filter
通过查看 polyfill
源码,可知
内部是通过 while 循环
的方式调用 callback
,这个循环没有使用 async/await
,不会等待异步任务执行完成
因此 forEach
、map
、filter
会出现近似于同时执行多个异步任务的情况
reduce
比较特殊
通过查看 reduce polyfill
源码,可知 reduce
也是通过 while 循环
的方式调用 callback
所以原理上和 forEach
、map
、filter
一样,也是同时执行多个异步任务
但是可以通过一些优化来实现 reduce + async/await
-
async
返回一个Promise
对象,所以callback
的total
需要处理then
函数 - 第一个
total
值或者reduce
初始值需要加工成Promise
对象
否则会报错
// reduce + async/await
function reduceFun() {
list.reduce(async (pre, next, index) => {
return await pre.then(() => setTimeoutFun(next))
}, Promise.resolve())
}
这里有一个有趣的点——为什么这么写 reduce
生效,forEach
不生效?
// 这里 forEachFun 依旧是同时执行
function forEachFun() {
list.forEach(async (num) => {
await Promise.resolve(num).then(() => setTimeoutFun(num))
})
}
// 这里 reduceFun 依旧可以等待上一个任务执行完成
function reduceFun() {
list.reduce(async (pre, next, index) => {
// return await Promise.resolve().then(() => setTimeoutFun(next)) // 不使用 pre 就同步进行
return await Promise.resolve(pre).then(() => setTimeoutFun(next)) // 使用 pre 就依次进行
}, Promise.resolve())
}
从网上找到的观点是:
如果 reduce callback
的 total
参数第一个出现并且参与计算
就可以让异步任务依次进行
听起来很扯,也没有从 MDN
说明和源码 polyfill
上找到合理的解释