详解async/await —— 从入门到实现原理

一、前言

如果大家都熟悉了 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 的作用,那么接下来我们去了解一下他的语法规则:

  • asyncfunction 的一个前缀,只有 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,如有问题,还请大家指出。

相关推荐
前端Hardy2 分钟前
HTML&CSS: 实现可爱的冰墩墩
前端·javascript·css·html·css3
web Rookie32 分钟前
JS类型检测大全:从零基础到高级应用
开发语言·前端·javascript
Au_ust40 分钟前
css:基础
前端·css
帅帅哥的兜兜40 分钟前
css基础:底部固定,导航栏浮动在顶部
前端·css·css3
工业甲酰苯胺43 分钟前
C# 单例模式的多种实现
javascript·单例模式·c#
yi碗汤园43 分钟前
【一文了解】C#基础-集合
开发语言·前端·unity·c#
就是个名称44 分钟前
购物车-多元素组合动画css
前端·css
编程一生1 小时前
回调数据丢了?
运维·服务器·前端
丶21361 小时前
【鉴权】深入了解 Cookie:Web 开发中的客户端存储小数据
前端·安全·web
Missmiaomiao2 小时前
npm install慢
前端·npm·node.js