likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 40

一、前言

如果大家都熟悉了 Promise,那么一定要来学习一个很重要的语法糖—— async/await 通过同步的方式执行异步任务。本文将会帮助读者从入门到手撕 async/await,同时分享一些自己对异步任务问题处理的思路。

二、async/await入门

2.1 理解作用

没学过的朋友可能会问:“同步方式执行异步任务体现在哪里呢?”。我们用一个🌰来对比说明一下。

现在我们要实现一个红绿灯的效果,通过异步任务依次输出 绿、黄、红 (时间的模拟暂时不考虑) 如果用传统的 Promise 实现:

Promise.resolve('绿').then((res) => {
  console.log(res)

  Promise.resolve('黄').then((res) => {
    console.log(res)

    Promise.resolve('红').then((res) => {
      console.log(res)
    })
  })
})

我们可以用 async/await 来实现一下:

// 立即执行函数
(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函数基本语法

先上一个代码示例

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() 只会一直输出第一步的结果。 因为 Generator 函数返回一个迭代器对象(也称为 Generator 对象)。这个迭代器对象可以用于逐步执行 Generator 函数内部的代码。 生成器函数中使用 yield 关键字来定义迭代的每一步,当调用 next() 方法时,生成器函数会从上一次暂停的位置继续执行,直到遇到下一个 yield 或函数结束。 因此,为了执行 Generator 函数的代码,我们需要先创建一个生成器对象,然后通过这个对象的 next() 方法来逐步执行代码。这样做的好处是可以保留生成器函数的状态,使得在每次执行 next() 时可以从上一次暂停的位置继续执行,而不是重新开始。(感谢评论区大佬的提醒,补充一下)

yield 后面返回的是一个函数,则输出的 value 取决于函数的返回值。因此我们可以返回一个 Promise 对象,再通过对 value 调用 then 获取值,以下是一个🌰:

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() 是可以传参的(但第一次传参没有效果),因此我们可以围绕这个功能来实现传参的需求,不废话直接上代码:

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 对象的函数:

function* generator() {
 // ……
}
1
function higherOrderFn(generatorFn) {
  return () => {
    return new Promise((resolve, reject) => {
        // 这里处理then回调的逻辑
    })
  }
}

console.log(higherOrderFn(generator())()) // Promise

接下来,我们再处理嵌套调用的问题。 直接上代码:

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 实现再适合不过了 伪代码:

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 解决问题)

伪代码:

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,如有问题,还请大家指出。

转载自:https://juejin.cn/post/7288963802649608250
评论
请登录