JS 异步 ( 三、generator 的用法、async/await 的用法 )


相关阅读:

一、生成器


1. 概念


(1) 作用

生成器可以暂停、恢复程序执行,搭配 Promise 可以用代码逻辑更紧凑、更易读的方式解决回调地狱。

(2) 关键字 yield

需要写在生成器函数内,当执行时遇见 yield 关键字时,先执行完 yield 声明的代码,然后暂停函数的执行

(3) 定义生成器

在关键字 function 和函数名中间加上符号 *, 即可定义一个生成器,如: function * generator(){}

(4) 生成器函数的调用

调用生成器函数时,并不会真正的执行生成器函数,而是会返回生成器的迭代器对象( Iterator 接口

(5) 迭代器的 next 方法

调用 next 方法,可以执行或恢复被 yield 暂停的生成器函数,next 方法最终会返回一个迭代器结果对
象( IteratorResult 接口 ),其属性值取决于以下两种情况:

执行中碰见 yieldvalue 属性值done 属性值
yield 声明的内容false,代表生成器函数被暂停,可以再次调用 next 方法
函数返回值true,代表生成器函数执行完成,不需再调用 next 方法

2. 基本使用


(1) 使用生成器可以分为三步:
① 定义生成器
② 调用生成器,得到迭代器对象
③ 调用迭代器的 next 方法,得到 IteratorResult 对象,来决定后续操作

(2) 基础语法演示

<script>
  // 第一步:定义生成器
  function * generator(){
    console.log('1')
    console.log('2')
    console.log('3')
    yield '这句话会被返回'
    console.log('4')
  }
  // 第二步:调用生成器,得到迭代器对象
  let genIterator = generator()

  // 第三步:调用迭代器的 next 方法,得到 IteratorResult 对象
  const first = genIterator.next()

  // 打印 IteratorResult 对象
  console.log("调用 next 后,返回的 IteratorResult 对象为: ")
  console.log("value: " + first.value)
  console.log("done: " + first.done)

  // 继续调用 next ,恢复被 yield 暂停的函数
  if(!first.done){
    genIterator.next()
  }
  // 输出:
  // 1
  // 2
  // 3
  // 调用 next 后,返回的 IteratorResult 对象为:
  // value: 这句话会被返回
  // done: false
  // 4
</script>

(3) yield 修饰的内容,暂停恢复后会丢失

来看一段代码,预想输出结果是 ares5k,但实际却是 undefined:

<script>
  // 定义函数
  function getName(){
    return 'ares5k'
  }
  // 定义生成器
  function * generator(){
    let name = yield getName()
    console.log(name)
  }
  let genIterator = generator() // 取得迭代器
  genIterator.next() // 执行到第一个 yield 后暂停
  genIterator.next() // 恢复执行
  // 输出:undefined
</script>

通过输出结果可以分析大概流程:

(1) 第一次调用 next 方法时,会在 let name = yield getName() 这一行发生暂停,赋值操作是从右到左,此时右侧执行
完就已经暂停了,左侧赋值操作还未进行

(2) 第二次调用 next 方法,会恢复函数执行,继续执行 let name = yield getName() 的左侧赋值部分,而此时赋的并
不是预期的 getName() 的结果, 而是一个 undefined,因此可以得出结论,yield 修饰的内容,暂停恢复后会丢失


解决 yield 修饰的内容丢失问题:

既然是在恢复时丢失了值,那么只要在恢复执行的那个 next 中,传入 yield 修饰的值即可,而 yield 修饰的内容,
会在前一个 next 暂停时以迭代器结果对象返回( IteratorResult 接口 ),具体实现如下:

<script>
  // 定义函数
  function getName(){
    return 'ares5k'
  }
  // 定义生成器
  function * generator(){
    let name = yield getName()
    console.log(name)
  }
  let genIterator = generator() // 取得迭代器
  const genIteratorResult = genIterator.next() // 取得迭代器结果对象
  genIterator.next(genIteratorResult.value) // 恢复执行,并传入 yield 修饰的内容
  // 输出:ares5k
</script>

3. 解决回调地狱

用生成器 + Promise 来实现之前文章里每隔 500 毫秒分别输出 a b c d e 的例子

<script>
  // 定义函数
  let fn = word => {
    return new Promise(resolve => {
      setTimeout(function () {
        console.log(word)
        resolve()
      }, 500)
    })
  }
  // 业务的逻辑更紧凑,更易读,就像写同步代码一样
  function * generator() {
    yield fn('a')
    yield fn('b')
    yield fn('c')
    yield fn('d')
    yield fn('e')
  }
  // 按步骤执行各个任务,
  const genIterator = generator()
  genIterator.next().value.then(()=>{
    genIterator.next().value.then(()=>{
      genIterator.next().value.then(()=>{
        genIterator.next().value.then(()=>{
          genIterator.next()
        })
      })
    })
  })
</script>

可以看出,生成器 + Promise 实现的代码,业务部分的代码,逻辑更紧凑,更易读,就像写同步代码一样,但
是在生成器的调用阶段,就很麻烦,甚至又出现了回调地狱,而且调用多少次 next 都是预先定义好的,不灵活


实现生成器的自动执行

分析上面例子中的回调地狱,我们可以发现它与平时的回调地狱不同,它的回调内部没有很多的业务逻辑代码,仅
仅是调用 next 恢复函数执行而已,了解了这个点,我们就可以对其进行改造:

<script>
  // 定义函数
  let fn = word => {
    return new Promise(resolve => {
      setTimeout(function () {
        console.log(word)
        resolve()
      }, 500)
    })
  }
  // 业务的逻辑更紧凑,更易读,就像写同步代码一样
  function * generator() {
    yield fn('a')
    yield fn('b')
    yield fn('c')
    yield fn('d')
    yield fn('e')
  }
  // 生成器的自动执行器
  function auto(generator){
    function next(data){
      const result = genIterator.next(data)
      if (result.done) return result.value
      result.value.then(function(data){
        next(data)
      })
    }
    const genIterator = generator();
    next();
  }
  // 调用生成器的自动执行器
  auto(generator);
</script>

改造后的代码清爽,易读
理解了上面自动执行器的原理,就可以在实际开发中根据需求对其做出调整,来适应自己的场景


二、async / await


1. 作用


async + await 是比使用生成器或 Promise 更简洁的回调地狱处理方案,也有人把其看作生成器 + Promise + 自执行的语法糖。

2. 关键字 async


(1) 定义 async 函数

在关键字 function箭头函数的参数列表前写 async,如:async function fn(){}const fn = async () => {}

(2) async 函数的返回值

不管代码中怎么写,async 函数都一定会返回一个 Promise 对象,具体规则如下:

async 函数中定义的返回值async 函数的实际返回值Promise 对象内部状态
Promise 对象直接返回该 Promise 对象状态由返回的 Promise 对象决定
Promise 以外的值返回 Promise,原返回值会当作参数传入处理程序对象状态为 fulfilled
未显式返回内容返回 Promise,不会对处理程序传参对象状态为 fulfilled
函数发生异常返回 Promise 对象,异常原因会当作参数传入处理程序对象状态为 rejected

(3) async 函数返回场景列举

<script>
  // 场景一:函数执行异常
  const withError = async function () {
    throw Error
    return '异常后的内容不会被执行'
  }
  withError().catch(data => {
    console.log('场景一:函数执行异常时,会返回一个状态为 rejected 的 Promise 对象,异常原因:' + data)
  })

  // 场景二:未显式返回内容
  const nothing = async function () {
  }
  nothing().then(() => {
    console.log('场景二:未显式返回内容时,会返回一个状态为 fulfilled 的 Promise 对象,且处理程序无参')
  })

  // 场景三:显式返回 Promise 以外的值
  const notPromise = async function () {
    return '这是原返回值,现被当作参数传入处理程序'
  }
  notPromise().then(data => {
    console.log('场景三:显式返回 Promise 以外的值时,会返回一个状态为 fulfilled 的 Promise 对象,处理程序参数:' + data)
  })

  // 场景四:显式返回 状态为 fulfilled 的 Promise 对象
  const fulfilledPromise = async function () {
    return new Promise(resolve => {
      resolve('成功')
    })
  }
  fulfilledPromise().then(data => {
    console.log('场景四:显式返回 状态为 fulfilled 的 Promise 对象,直接返回该对象')
  })

  // 场景五:显式返回 状态为 rejected 的 Promise 对象
  const rejectedPromise = async function () {
    return new Promise((resolve, reject) => {
      reject('失败')
    })
  }
  rejectedPromise().catch(data => {
    console.log('场景五:显式返回 状态为 rejected 的 Promise 对象')
  })
</script>

3. 关键字 await


(1) 概念

必须写在 async 函数内,当执行到 await 语句时,await 修饰的语句会正常执行,这个语句执行完成后,该 async
函数会将其余代码加入到 <微队列> 中,在主线程任务空闲时,再恢复 async 函数剩余部分的执行。


await 修饰的内容:

await 修饰 Promise 对象时:
Ⅰ. Promise 对象必须调用 resolvereject, 否则该 await 之后的代码不会执行
Ⅱ. await 修饰 Promise 语句之后的代码会一直等待,直到 Promise 调用完 resolvereject 后在执行

await 修饰非 Promise 对象时:
Ⅰ. 不管 await 修饰的代码是否执行完成,之后的代码都会同步执行,不会等待

(2) await 场景列举,进一步熟悉 await 特点

① 场景一:await 标准执行顺序

await 修饰的内容执行 -> 主线程逻辑执行 -> async 函数剩余代码执行 -> setTimeout 执行

<script>
  setTimeout(function () {
    console.log(1)
  })
  const noResolveOrReject = async function () {
    await console.log(2)
    console.log(3)
    console.log(4)
  }
  noResolveOrReject()
  console.log(5)
  // 输出:2,5,3,4,1
</script>

② 场景二:await 修饰 Promise 对象,后面的代码会一直等待,直到 Promise 调用完 resolvereject 后在执行
如果 await 前是赋值操作,那么 resolvereject 的参数,会赋值给该变量

await 修饰的内容执行 -> 主线程逻辑执行 -> 回到 async 函数 -> 函数内剩余代码等待调用 resolvereject 后执行

<script>
  const noResolveOrReject = async function () {
    const value = await new Promise(resolve => {
      console.log(1)
      setTimeout(() => resolve('会返回给 await 的数据'), 3000)
    })
    console.log(value)
    console.log(2)
    console.log(3)
  }
  noResolveOrReject()
  console.log(4)
  // 输出:1,4,会返回给 await 的数据,2,3
</script>

③ 场景三:await 修饰的 Promise 未调用 resolvereject 语句,async 函数的剩余代码不会执行

await 修饰的内容执行 -> 主线程逻辑执行 -> 回到 async 函数 -> 函数内剩余代码不会执行

<script>
  setTimeout(function () {
    console.log(1)
  })
  const noResolveOrReject = async function () {
    await new Promise(() => console.log(2))
    console.log(3)
    console.log(4)
  }
  noResolveOrReject()
  console.log(5)
  // 输出:2,5,1
</script>

④ 场景四:await 修饰的是非 Promise 对象,不管其是否执行完,后面的代码都会同步执行

await 修饰的内容执行 -> 主线程逻辑执行 -> 回到 async 函数 -> 剩余代码不会等待 setTimeout 直接执行

<script>
  const noResolveOrReject = async function () {
    await setTimeout(() => console.log(1), 3000)
    console.log(2)
    console.log(3)
  }
  noResolveOrReject()
  console.log(4)
  // 输出:4,2,3,1
</script>

4. 常见案例

按顺序执行异步任务

<script>
  function task1() {
    return new Promise(resolve => {
      console.log('异步任务1')
      resolve('第一个任务结果')
    })
  }
  function task2(param) {
    console.log('异步任务2:接收到第一个任务结果:' + param)
  }
  async function logic() {
    const value = await task1()
    task2(value)
  }
  logic()
  // 输出:
  // 异步任务1
  // 异步任务2:接收到第一个任务结果:第一个任务结果
</script>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值