async/await详解+实战演练
前言
最近做了一个项目, 是国内做前端开发, 绕不开的一个平台: 微信生态! 之前做过微信小程序, 这回这个则是微信公众号页面, 个人觉得微信小程序的开发体验比微信公众号页面的开发体验更好, 私以为是因为小程序的授权以及各种API
更完善, 当然了, 也可能是因为我做的这个小程序比较简单吧, 关于微信公众号页面的具体内容可查看这篇文章: vue3+vant开发微信公众号网页爬坑不完全指北
这次的微信公众号开发前前后后有一个月的时间, 业务逻辑不算复杂, 但也不少, 主要是和微信进行各种交互, 各种流程, 这就少不了异步逻辑
的处理, 对于前端开发来说, 异步逻辑的解决方案, 最著名和好用的莫过于async/await+promise
了, 关于这个异步解决方案, 我之前也发过一篇相关的文章, 感兴趣的小伙伴可以看看: ES8 async/await: 优雅的异步编程解决方案, 而我个人还看过一篇非常著名的阮一峰老师的文章, 也就是大名鼎鼎的ECMAScript 6入门
里的这篇文章: async 函数_ECMAScript 6 入门
本篇文章主要是从一些概念原理, 尤其是实际编写之后才发现的出发, 聊一聊我遇到的async/await+promise
的实际使用场景, 那么话不多说, 我们正式开始
Promise
Promise.all等静态方法本文就不讨论了, 有需要的小伙伴可查阅阮一峰老师这本书的Promise相关章节: Promise 对象
首先还是我们所熟悉的Promise
对象, 由Promise
构造函数
生成:
const promise = new Promise((resolve, reject) => {
//...
if(/* 异步操作成功 */) {
resolve(value);
}else{
reject(error);
}
});
Promise
构造函数
接收一个函数作为参数, 这个函数还有两个参数, 第一个参数我们叫resolve
, 第二个我们叫reject
, 分别是解决
和拒绝
的意思
返回的Promise
对象一共有3种状态: pending
, fulfilled
以及rejected
我们通过new
操作符调用Promise
的时候就是执行它的构造函数
, 此时也会执行构造函数
的参数, 以上面的代码为例就是会执行传递到Promise
中的箭头函数
, 此时返回的Promise
对象的状态为pending
, 当我们调用resolve
或者reject
的时候, Promise
对象的状态将会改变: 调用resolve
会使Promise
对象的状态由pending
变为fulfilled
, 调用reject
则会使其状态从pending
变为rejected
同时需要注意的是, Promise
的状态只能被改变一次, 要么从pengding
变为fulfilled
, 要么从pending
变为rejected
, fulfilled
和rejected
之间不能互相变换更不能变回pending
, 以及状态从pending
变为rejected
的时候将会抛出一个错误
resolve
被调用时传递的参数将会变成Promise
对象的结果, 该结果可以通过Promise
对象的then
方法获取到, 而reject
被调用时传递的参数则会被Promise
对象的catch
方法获取到:
promise
.then((res) => {
//res就是调用resolve方法时传递的参数
console.log(res);
})
.catch((error) => {
//error是调用reject方法时传递的参数
console.log(error);
});
也就是说Promise
对象的状态为fulfilled
(调用了resolve
方法之后)时, 我们能调用then
方法获取resolve
传递出来的参数, 而当它的状态是rejected
(调用了reject
方法之后)时, 我们则能调用catch
方法获取reject
传递出来的参数
这里能使用链式调用
是因为Promise
的then
方法和catch
方法都会返回一个Promise
对象
async/await
await
await
关键字必须要在async
函数中使用, await
的功能就像它的意思一样, 等待
, 表示后面的表达式要等待一个结果, 而这个表达式一般是返回一个Promise
对象, 也就是说await
后面一般跟能返回Promise
对象的表达式, 同时只有await
等待的结果为fulfilled
的时候才会执行它后面的语句, pending
和rejected
的时候都不会执行
简单来说就是await
关键字会等待它后面的Promise
fulfilled
之后将Promise
的结果做一个返回, 然后再接着执行后续的语句
由于await
后面需要跟一个能返回Promise
的表达式, 因此我们这里写一个函数, 它的返回值是一个Promise
, 我这里用的是箭头函数
, 它本质上是一个函数表达式
, 写成函数声明
的形式也是可以的, 但个人习惯, 因此还是写成箭头函数
的形式, 这里也附上函数声明
的写法:
async function foo() {
}
后续的写法均会使用箭头函数
的形式:
const p = () => {
return new Promise((resolve, reject) => {
setTimeout(
() => {
resolve(123);
},
2000
)
});
}
const foo = async () => {
const res = await p();
console.log(res);
console.log('后续代码');
}
foo();
此时2
秒之后才会打印res
和后续代码
, 因为new
的时候Promise
的状态为pending
, 2
秒之后变为了fulfilled
, 而如果是这样的情况:
const p = () => {
return new Promise((resolve, reject) => {
});
}
const foo = async () => {
const res = await p();
console.log(res);
console.log('后续代码');
}
foo();
await
后面的两个输出语句永远不会执行, 因为此时await
后面的表达式返回的Promise
对象的状态是pending
的, 接下来再看看这段代码:
const p = () => {
return new Promise((resolve, reject) => {
reject('一个错误');
});
}
const foo = async () => {
const res = await p();
console.log(res);
console.log('后续代码');
}
foo();
此时await
后面的两个输出语句也是永远不会执行, 因为Promise
对象的状态是rejected
的, 也就是说: async
函数中, await
语句后面的语句要执行, 当且仅当这个await
后面的Promise
的状态是fulfilled
的时候才行
而await
最典型的用法就是继发
的操作, 一个执行完成再执行下一个:
const p1 = () => {
return new Promise((resolve, reject) => {
setTimeout(
() => {
resolve('p1');
},
2000
)
});
}
const p2 = () => {
return new Promise((resolve, reject) => {
resolve('p2');
});
}
const foo = async () => {
const res1 = await p1();
const res2 = await p2();
console.log(res1);
console.log(res2);
}
foo();
这里2
秒之后p1
的状态变为fulfilled
, 然后p2
才会执行
这里可能有小伙伴要问了: rejected
了怎么办? 这个问题我会放到接下来的内容中和大家探讨
async
现在我们知道await
关键字必须要在async
函数中才能使用, 相当于async
给await
提供了一个作用域, 但除此之外async
还有什么别的作用吗? 私以为是有的, 因为我们的async
函数只要执行了, 就会返回一个Promise
对象, 而且这个Promise
对象的状态默认是fulfilled
的, 但如果async
函数内显式
返回了一个Promise
对象, 那么最终async
函数返回的Promise
就是里面显式
返回的Promise
对象, 最终状态由内部显式
返回的Promise
对象决定, 简单来说就是:
async
内返回值不是Promise
: 那个返回值会被Promise.resolve()
处理之后返回, 此时async
返回的Promise
对象的状态为fulfilled
async
内返回了一个Promise
: 直接返回这个Promise
, 此时async
返回的Promise
对象的状态为内部Promise
的状态
返回值不是Promise对象
const foo = async () => {
}
const res = foo();
console.log(res);
此时我们可以看到打印的res
是个Promise
对象, 同时它的状态为fulfilled
, 只是结果是undefined
, 因为async
函数内没有任何的return
语句来返回一个值:
const foo = async () => {
}
foo().then((res) => {
console.log('res:', res);
});
这里可以看到打印的res
是undefined
, 此时我们尝试另一种写法:
const foo = async () => {
return 123;
}
foo().then((res) => {
console.log('res:', res);
});
此时打印的res
就有值了, 是123
返回值是Promise对象
我们直接来看代码:
const p = () => {
return new Promise((resolve, reject) => {
reject('p rejected');
});
}
const foo = async () => {
return p();
}
foo()
.then((res) => {
console.log('res:', res);
})
.catch((error) => {
console.log('error:', error);
});
上面这段代码中, p
函数返回了一个rejected
的Promise
, 然后async
函数foo
中直接返回这个Promise
, 也就是说async
的返回值已经是一个Promise
对象了, 所以最终async
返回的Promise
就是p
函数返回的Promise
, 此时会进到catch
回调, 因为p
函数返回的是一个rejected
的Promise
返回值前加不加await
先说结论哈: 个人认为是没必要加, 因为加不加结果都一样
async
函数中返回Promise
还有一种写法是在Promise
前面加await
关键字, 像这样:
const p = () => {
return new Promise((resolve, reject) => {
resolve('p resolved');
});
}
const foo = async () => {
return await p();
}
foo()
.then((res) => {
console.log('res:', res);
})
.catch((error) => {
console.log('error:', error);
});
再对比看一下这段代码:
const p = () => {
return new Promise((resolve, reject) => {
resolve('p resolved');
});
}
const foo = async () => {
return p();
}
foo()
.then((res) => {
console.log('res:', res);
})
.catch((error) => {
console.log('error:', error);
});
两段代码唯一的区别在于foo
函数内的return
语句, 前者有await
关键字, 后者没有, 但最终结果是一样的, 都走到了foo
的then
回调中, 为何会是一样的呢?
在讨论这个问题之前, 我们把这两段代码修改一下:
const p = () => {
return new Promise((resolve, reject) => {
resolve('p resolved');
});
}
const foo = async () => {
const res = await p();
console.log('res:', res);
}
foo();
const p = () => {
return new Promise((resolve, reject) => {
resolve('p resolved');
});
}
const foo = async () => {
const res = p();
console.log('res:', res);
}
foo();
关键代码在foo
函数内, 一个是打印await p()
, 一个是直接打印p()
, 根据上面Promise
和await
的知识能得出: await p()
的返回值是字符串
p resolved
, 而p()
的返回值则是一个Promise
, 再根据async
的知识: async
返回的结果是Promise
则直接返回, 不是则会通过Promise.resolve
处理之后返回可得出: 字符串
p resolved
不是Promise
对象, 它会被Promise.resolve
处理成Promise
, 处理成和p
函数的返回值一样, 也就是说最终的结果都是p
函数的返回值, 所以加不加await
关键字, 结果都是一样的
处理多个异步操作
async
函数有一个常用的做法是将多个await
收敛起来做统一的处理, 也就是处理多个异步操作:
const p1 = () => {
return new Promise((resolve, reject) => {
resolve('p1');
});
}
const p2 = (params) => {
return new Promise((resolve, reject) => {
resolve(`${params}_p2`);
});
}
const foo = async () => {
const res1 = await p1();
const res2 = await p2(res1);
return res2;
}
foo().then((res) => {
console.log('res:', res);
});
这个使用方式也是async/await
最常用的一个方式, 就是继发
逻辑的处理, 而根据上面的知识, foo
函数内也可以这么写:
const foo = async () => {
const res1 = await p1();
return p2(res1);
}
一个细节
async
函数一经调用就会返回一个fulfilled
的Promise
对象, 我们无法在其中根据条件来修改这个Promise
对象的状态, 比如:
const foo = async () => {
let res = 1;
setTimeout(
() => {
res = 2;
},
3000
);
return res;
}
foo()
.then((res) => {
console.log('res:', res);
})
.catch((error) => {
console.log('error:', error);
});
这段代码执行之后会走到foo().then
方法中, 打印1
, 哪怕使用了await
关键字也不行:
const foo = async () => {
let res = 1;
await setTimeout(
() => {
res = 2;
},
3000
);
return res;
}
foo()
.then((res) => {
console.log('res:', res);
})
.catch((error) => {
console.log('error:', error);
});
这段代码的运行结果和上面那段代码是一样的, 因此async
函数的主要作用就是用来给await
提供一个执行的作用域, 让我们能以同步的方式处理异步的逻辑
也就是说, 当我们需要处理一些逻辑, 在这些逻辑没有一个结果的时候Promise
需要pending
等待我们处理, 然后我们再根据处理的结果来决定这个Promise
的状态究竟是fulfilled
还是rejected
, 此时只能使用一开始提到的Promise
构造函数
来实现
错误处理
错误情况的处理是对await
后面的Promise
对象而言的, 但await
又必须要在async
方法中使用, 因此这里结合async
来探讨, 以及这里主要聊一聊常用的几个情形
报错之后不中断后续代码执行
const p = () => {
return new Promise((resolve, reject) => {
reject('p rejected');
})
}
const foo = async () => {
const res = await p().catch((error) => {
console.log('error:', error);
});
console.log('res:', res);
console.log('后续代码');
}
foo();
这样的写法能捕获到报错, 同时还不会中断后续代码的执行
还有一个种写法是使用try...catch...
语句:
const p = () => {
return new Promise((resolve, reject) => {
reject('p rejected');
})
}
const foo = async () => {
try {
const res = await p();
console.log('res:', res);
} catch (error) {
console.log('error:', error);
}
console.log('后续代码');
}
foo();
但一般try...catch...
语句主要用于有多个await
的情况
报错之后中断后续代码执行
这个是比较常见的一个情形, 比如这样的一段代码:
const p1 = () => {
return new Promise((resolve, reject) => {
reject('p1 rejected');
})
}
const p2 = () => {
return new Promise((resolve, reject) => {
resolve('p2 resolved');
})
}
const foo = async () => {
try {
const res1 = await p1();
console.log('res1:', res1);
const res2 = await p2();
console.log('res2:', res2);
} catch (error) {
console.log('error:', error);
}
}
foo();
由于await
的特性, 当p1
返回的Promise
rejected
之后, 后续的代码就不会执行了, 而对于多个await
且上一个报错要中断下一个的执行, 那么用try...catch...
语句来处理再好不过了
但上述的错误处理都是在async
函数内进行的, 还有一种是需要将报错返回出去的情况, 当然了, 实际情况是需要将async
内得到的结果返回出去, 无论是否报错, 未报错的情形上面已经探讨过了, 这里主要来看看报错的情形
将async中的成功/错误结果返回
先说返回错误的情形:
const p1 = () => {
return new Promise((resolve, reject) => {
reject('p1 rejected');
})
}
const p2 = () => {
return new Promise((resolve, reject) => {
resolve('p2 resolved');
})
}
const foo = async () => {
const res1 = await p1();
console.log('res1:', res1);
const res2 = await p2();
console.log('res2:', res2);
console.log('error:', error);
}
foo()
.then((res) => {
console.log('res:', res);
})
.catch((error) => {
console.log('error:', error);
});
这样就可以了, 就能将错误返回出去了, 任意一个await
后面的Promise
rejected
了, 后续代码就会终止执行, 同时这种情况还等同于async
返回的Promise
对象被reject
了, 可以理解为async
会捕获它里面的错误, 确切的说是异常, 当然错误Error
也算一种异常, 比如:
const foo = async () => {
throw '出错了'; //抛出一个值为字符串 出错了 的异常
}
foo()
.then((res) => {
console.log('res:', res);
})
.catch((error) => {
console.log('error:', error);
});
const foo = async () => {
throw new Error('出错了'); //抛出一个消息内容为 出错了 的Error对象
}
foo()
.then((res) => {
console.log('res:', res);
})
.catch((error) => {
console.log('error:', error);
});
再来就是更常见的情况: 将最终结果返回, 无论这个结果是否报错, 此时我们可以这么做:
const p1 = () => {
return new Promise((resolve, reject) => {
resolve('p1');
})
}
const p2 = (params) => {
return new Promise((resolve, reject) => {
resolve(`${params}_p2 resolved`);
})
}
const foo = async () => {
const res1 = await p1();
return p2(res1);
}
foo()
.then((res) => {
console.log('res:', res);
})
.catch((error) => {
console.log('error:', error);
});
这个情形中p1
p2
都fulfilled
了, foo
中的代码依次执行, 最终返回p2
fulfilled
的Promise
, 再来就是报错:
const p1 = () => {
return new Promise((resolve, reject) => {
reject('p1 rejected');
})
}
const p2 = (params) => {
return new Promise((resolve, reject) => {
resolve(`${params}_p2 resolved`);
})
}
const foo = async () => {
const res1 = await p1();
return p2(res1);
}
foo()
.then((res) => {
console.log('res:', res);
})
.catch((error) => {
console.log('error:', error);
});
此时由于p1
报错, 则后续p2
不会执行, 会直接报错, 同时被async
捕获, 所以此时async
函数返回的Promise
是p1
rejected
的Promise
async中使用try...catch...并需要返回错误结果
这是我个人在实际开发工作中遇到的一个情况, 大致代码如下:
const p1 = () => {
return new Promise((resolve, reject) => {
reject('p1 rejected');
})
}
const p2 = (params) => {
return new Promise((resolve, reject) => {
resolve(`${params}_p2 resolved`);
})
}
const foo = async () => {
try {
const res1 = await p1();
return p2(res1);
} catch (error) {
return error;
}
}
foo()
.then((res) => {
console.log('res:', res);
})
.catch((error) => {
console.log('error:', error);
});
foo
中执行了一个继发
的异步操作, 成功则返回最终的结果, 也就是p2
执行之后的结果, 任意一个报错了则返回这个错误, 但这个代码最终的执行结果却是进到了foo().then
方法中, 而不是预期的foo().catch
中, 这是为什么呢?
这是因为try...catch...
中的catch
捕获到的那个error
已经不是一个Promise
了, 而是一个异常, 在上面这个代码中, 这个异常
的类型是字符串
, 而从上面async
的知识中我们知道: 返回的结果如果不是Promise
那么它会使用Promise.resolve
处理一下然后再返回, 也就是说, 上面的foo
函数中的代码等价于:
const foo = async () => {
try {
const res1 = await p1();
return p2(res1);
} catch (error) {
return Promise.resolve(error);
}
}
这么写也会进到foo().then
方法中
项目中我在其他地方调用foo
函数的时候遇到里面报错, 但最终foo
函数返回的Promise
居然是fulfilled
的情况, 多方排查之后才发现了这个问题, 最终我将foo
函数改为如下的形式之后, 运行结果就符合预期了:
const foo = async () => {
try {
const res1 = await p1();
return p2(res1);
} catch (error) {
return Promise.reject(error);
}
}
使用Promise.reject
处理一下这个字符串
, 那最终返回的就是rejected
的Promise
了
但结合上面的知识我们不难发现, 这个foo
函数还有一个写法也能符合我们的预期:
const foo = async () => {
const res1 = await p1();
return p2(res1);
}
p1
报错, async
能捕获这个错误并直接返回, p2
报错也不用担心, 因为我们显式地写了return
关键字
实际案例
一次请求多个接口渲染页面
这个私以为也是最常见的情形
比如这里有两个接口:
const api = {
fetch1(delay) {
return new Promise((resolve, reject) => {
setTimeout(
() => {
resolve({
data: 1,
success: 1,
msg: 'done'
})
},
delay
);
})
},
fetch2(delay) {
return new Promise((resolve, reject) => {
setTimeout(
() => {
resolve({
data: 2,
success: 1,
msg: 'done'
})
},
delay
);
})
}
};
每次请求都渲染页面
这里就会有些出入了, 首先我们来看看每次请求都渲染页面
, 那么此时我们有两个设置数据的方法, 对应两个请求:
handleSetData1
:
handleSetData1 = async () => {
const res = await api.fetch1(2000);
if(res.success) {
//设置数据
return true;
}
return Promise.reject(res.msg);
}
handleSetData2
:
handleSetData2 = async () => {
const res = await api.fetch2(100);
if(res.success) {
//设置数据
return true;
}
return Promise.reject(res.msg);
}
为了模拟真实的请求, 这里分别给它们做了延迟处理. 然后还有一个统一的请求数据的方法:
getData
:
getData = () => {
Promise.all([
this.handleSetData1(),
this.handleSetData2()
]);
}
两个接口, 我们有各自的方法去请求并且做设置数据渲染页面的操作, 然后还有一个统一的getData
的方法调用, 看似没有问题, 但却可能造成多次渲染的问题, 比如上述代码, 两个接口分别有1秒和100毫秒的延迟, 这会导致延迟更短的那个先执行, 然后渲染页面, 接着延迟长的后执行, 然后再次渲染页面, 造成多次渲染的问题, 但我们的目的是接口请求都回来之后统一渲染, 毕竟页面依赖两个接口的数据
理想情况下两个接口延迟非常之短, 那可能会只渲染一次, 但我们写代码, 不能寄希望于理想情况, 反而应该考虑一些边界问题, 比如这里的重复渲染问题, 既然需要请求完毕所有接口再渲染页面, 那么我们就改一改
全部请求完成再渲染页面
还是那两个api
, 此时我们修改代码, 等它们都请求完毕之后再渲染页面:
getData
:
getData = async () => {
const res = await Promise.all([
api.fetch1(2000),
api.fetch2(100)
]);
const [ res1, res2 ] = res;
if(res1.success && res2.success) {
this.setState({
a: res1.data,
b: res2.data
})
}
}
这样就能减少一次额外的渲染
微信公众号权限验证配置
关于微信公众号开发相关的内容在这篇文章中有聊到, 需要的可以看一看: vue3+vant开发微信公众号网页爬坑不完全指北
handleWxConfig
:
const handleWxConfig = (jsApiList) => {
return new Promise((resolve, reject) => {
api
.retrieveWxJsSdkConfig() //这里可以换成实际的请求weixin-js-sdk配置的api
.then((res) => {
if (res.code === 0) {
wx.config({
// 开启调试模式,调用的所有 api 的返回值会在客户端 alert 出来,若要查看传入的参数,可以在 pc 端打开,参数信息会通过 log 打出,仅在 pc 端时才会打印。
debug: false,
appId: res.data.appId, // 必填,公众号的唯一标识
timestamp: res.data.timestamp, // 必填,生成签名的时间戳
nonceStr: res.data.nonceStr, // 必填,生成签名的随机串
signature: res.data.signature, // 必填,签名
jsApiList // 必填,需要使用的 JS 接口列表
});
wx.ready(function () {
// config信息验证后会执行 ready 方法,所有接口调用都必须在 config 接口获得结果之后,config是一个客户端的异步操作,所以如果需要在页面加载时就调用相关接口,则须把相关接口放在 ready 函数中调用来确保正确执行。对于用户触发时才调用的接口,则可以直接调用,不需要放在 ready 函数中。
resolve(true);
});
wx.error(function (res) {
// config信息验证失败会执行 error 函数,如签名过期导致验证失败,具体错误信息可以打开 config 的debug模式查看,也可以在返回的 res 参数中查看,对于 SPA 可以在这里更新签名。
reject(res);
});
} else {
reject(res.msg);
}
});
});
};
微信公众号选择图片并获取本地图片数据
同样在vue3+vant开发微信公众号网页爬坑不完全指北中有提到
handleWxChooseImg
:
const handleWxChooseImg = () => {
return new Promise((resolve, reject) => {
wx.chooseImage({
count: 1, // 默认9
sizeType: ['original', 'compressed'], // 可以指定是原图还是压缩图,默认二者都有
sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有
success(res) {
resolve(res);
// 返回选定照片的本地 ID 列表,localId可以作为 img 标签的 src 属性显示图片
// var localIds = res.localIds;
},
fail(error) {
reject(error);
}
});
});
};
handleWxGetLocalImgData
:
const handleWxGetLocalImgData = (localId) => {
return new Promise((resolve, reject) => {
//获取本地图片
wx.getLocalImgData({
localId, // 图片的localID
success(res) {
// res.localData是图片的base64数据,可以用 img 标签显示
//上传图片(axios, post直接扔base64, 貌似会转成form data类型上传, 而且还没有key)
resolve(res);
},
error(error) {
reject(error);
}
});
});
};
handleGetImgBase64
:
const handleGetImgBase64 = async () => {
try {
const res1 = await handleWxChooseImg();
const res2 = await handleWxGetLocalImgData(res1.localIds[0]);
return res2;
} catch (error) {
return Promise.reject(error);
}
};
然后根据上面的知识, 不难发现我们还可以省略try...catch...
语句写成这样:
const handleGetImgBase64 = async () => {
const res1 = await handleWxChooseImg();
const res2 = await handleWxGetLocalImgData(res1.localIds[0]);
return res2;
};
但私以为还是保留try...catch...
语句比较好, 不然可能会产生歧义: 这要是报错了怎么办, 而有了try...catch...
就一目了然了, 毕竟try...catch...
表示对可能出现异常的语句做一个容错的处理, 代码也是先给人读, 然后顺带让机器执行的, 人要是阅读起来有障碍, 那机器执行得再顺畅也无济于事
好的, 这就是这篇文章的全部内容了, 欢迎大家在评论区和我一起交流探讨, 最后, 如果你觉得这篇文章写得还不错, 别忘了给我点个赞, 如果你觉得对你有帮助, 可以点个收藏, 以备不时之需, 想看更多知识干货欢迎关注哦~
参考文献:
转载自:https://juejin.cn/post/7163261830326910989