一、前言
如果大家都熟悉了 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,如有问题,还请大家指出。