一、前言
如果大家都熟悉了 Promise
,那么一定要来学习一个很重要的语法糖------ async/await
通过同步的方式执行异步任务 。本文将会帮助读者从入门到手撕 async/await
,同时分享一些自己对异步任务问题处理的思路。
二、async/await入门
2.1 理解作用
没学过的朋友可能会问:"同步方式执行异步任务体现在哪里呢?"。我们用一个🌰来对比说明一下。
现在我们要实现一个红绿灯的效果,通过异步任务依次输出 绿、黄、红 (时间的模拟暂时不考虑) 如果用传统的 Promise
实现:
js
Promise.resolve('绿').then((res) => {
console.log(res)
Promise.resolve('黄').then((res) => {
console.log(res)
Promise.resolve('红').then((res) => {
console.log(res)
})
})
})
我们可以用 async/await
来实现一下:
js
// 立即执行函数
(async () => {
await Promise.resolve('绿').then(res => console.log(res))
await Promise.resolve('黄').then(res => console.log(res))
await Promise.resolve('红').then(res => console.log(res))
})()
通过前后对比,我们可以发现,在需要异步任务按照顺序严格执行的情况下, async/await
可以避免嵌套过多的情况,取而代之的是简单易懂的同步形式代码。
2.2 语法简述
既然大家了解了 async/await
的作用,那么接下来我们去了解一下他的语法规则:
async
是function
的一个前缀,只有async
函数中才能使用await
语法async
函数是一个Promise
对象,有无resolve
取决于有无在函数中return
值await
后面跟的是一个Promise
对象,如果不是,则会包裹一层Promise.resolve()
大家可以再结合上面的代码来熟悉一下语法规则,其实就那么简单👊
当然同学们肯定不能止步于"会用",如何摸透原理、如何优美处理业务,才是重中之重!
三、async/await实现原理
那么接下来我们先从 async/await
的实现原理入手👊
async/await
是由 generator函数
来实现的,该函数属于 ES6
新特性,想进一步了解的同学可以看一下 MDN
的 文档说明
3.1 generator函数基本语法
先上一个代码示例
js
function* generator() {
yield 1;
yield 2;
yield 3;
}
const gen = generator();
console.log(gen.next()); // {"value":1,"done":false}
console.log(gen.next()); // {"value":2,"done":false}
console.log(gen.next()); // {"value":3,"done":false}
console.log(gen.next()); // {"done":true}
最显著的特征就是 function
后面的 *
,函数体中的 yield
由迭代函数的 next()
控制,在最后的 next
返回值取决于有无 return
值。
注意:一定要定义一个 gen
出来调用 next()
,持续调用 generator()
只会一直输出第一步的结果。
若 yield
后面返回的是一个函数,则输出的 value
取决于函数的返回值。因此我们可以返回一个 Promise
对象,再通过对 value
调用 then
获取值,以下是一个🌰:
js
function p(num) {
return Promise.resolve(num)
}
function* generator() {
yield p(1)
yield p(2)
}
const gen = generator();
const next1 = gen.next()
next1.value.then((res1) => {
console.log(res1)
const next2 = gen.next()
next2.value.then((res2) => {
console.log(res2)
})
})
// 1 2
这段代码就可以实现 async/await
的一部分功能,当然还存在上一次异步任务的参数传入下一次的情况,因此我们还需要对传参做进一步研究。
首先,进一步参考 MDN
中对 next()
的说明,我们发现 next()
是可以传参的(但第一次传参没有效果),因此我们可以围绕这个功能来实现传参的需求,不废话直接上代码:
js
function p(num) {
return Promise.resolve(num * 2)
}
function* generator() {
const value1 = yield p(1)
const value2 = yield p(value1)
return value2
}
const gen = generator();
const next1 = gen.next()
next1.value.then((res1) => {
console.log(res1)
const next2 = gen.next(res1)
next2.value.then((res2) => {
console.log(res2)
})
})
// 2 4
至此,虽然可以满足 async/await
的需求,但是我们发现代码中存在了多次的嵌套调用,这还取决于 yield
的数量,这明显是不能容忍的,与此同时,gen
最终返回的也不是一个 Promise
对象,因此我们可以通过一个高阶函数来解决问题。
3.2 高阶函数封装
所谓高阶函数,就是在函数中返回函数,那么我们就可以在高阶函数中返回一个返回值为 Promise
对象的函数:
js
function* generator() {
// ......
}
1
function higherOrderFn(generatorFn) {
return () => {
return new Promise((resolve, reject) => {
// 这里处理then回调的逻辑
})
}
}
console.log(higherOrderFn(generator())()) // Promise
接下来,我们再处理嵌套调用的问题。 直接上代码:
js
function p(num) {
return Promise.resolve(num * 2)
}
function* generator() {
const value1 = yield p(1)
const value2 = yield p(value1)
return value2
}
function higherOrderFn(generatorFn) {
return () => {
return new Promise((resolve, reject) => {
let gen = generatorFn()
// 链式处理yield
const doYield = (val)=>{
console.log(val)
let res
try{
res = gen.next(val)
}catch(err){
reject(err)
}
const {value,done} = res
// done === true 函数结束,resolve结果
if(done){
return resolve(value)
}else{
// 未结束,处理 value,同时传参
value.then((val)=>{doYield(val)})
}
}
doYield()
})
}
}
const asyncFn = higherOrderFn(generator)()
// undefined
// 2
// 4
完成到这一步,generator
的函数体已经能和 async
函数实现契合了,同学们可以自己手搓一下~ 对于高阶函数,后续也会抽时间来一篇文章总结一下😊
四、异步任务处理思路
理解了 async/await
的原理,我们还需要熟练面对各种异步任务处理的场景,接下来我会例举一些涉及到异步任务的业务场景,来分享一些我对异步任务的处理思路(持续更新👊)。
4.1 情景1------用户登录后获取用户数据
业务逻辑:
- 调用用户登录接口 ,返回
token
- 调用获取用户信息接口 ,携带
token
,返回userInfo
这里的逻辑很简单,存在一个排队的情况,用 async/await
实现再适合不过了
伪代码:
js
const login = async () => {
const loginRes = await loginAPI()
// loginAPI接口返回的token会存入localStorage,作为默认参数传递
// 因此getUSerInfo不用手动传token
const userInfoRes = await getUserInfo()
}
4.2 情景2------图片上传
业务逻辑:
- 通过
FormData
格式传输图片,返回图片url
- 在某表单提交的接口上传表单数据,携带
url
跟上个情景类似,但这边还存在批量图片上传的情况,这里可以使用 Promise.all
,这里解释一下这样使用的优缺点:
- 优点:不用排队,图片批量上传速度快
- 缺点:任何一个请求失败,返回
reject
(ps:可以用Promise.allSettled
解决问题)
伪代码:
js
const filePromsie = (file) => {
const formdata = new FormData()
formdata.append(file)
return fileSubmitAPI(formdata)
}
const submit = (files) => {
const promiseArr = []
files.length && files.forEach((file) => {
promiseArr.push(filePromsie(file))
})
Promise.all(promiseArr).then((res) => {
tableSubmitAPI({ res, ...tableData })
})
}
其实大部分的异步任务处理都很简单,偶然出现较为复杂的情景,还需要大家从原理出发,选择最好的处理方案,与此同时我也会持续加入一些复杂异步任务的处理方案,如有idea也可以告诉我!
五、结语
本文帮大家从入门到原理梳理了一遍 async/await
,如有问题,还请大家指出。