• 浅谈 async/await 和生成器


    浅谈 async/await

    async/await 是ES8规范新增的,使得以同步方式写的代码异步运行不再是白日梦,进一步让代码逻辑更加清晰。

    为什么新增 async/await

    下面有这样一个需求:有两个请求,请求 1 的结果是请求 2 的参数,所以请求 2 必须在请求 1 之后发出,使用 Promise 实现如下:

    request('/xxx').then((res) =>{
      request('/yyyy',{res}).then(() => {
        // 对结果进行处理
      })
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5

    可以看到这里形成了一个嵌套结构,试想如果这样的嵌套层级深了以后就会形成类似“回调地狱”那样的结构,很不利于维护。因此就新增了 async/await 这两个关键字来解决这个问题。上面的需求 async/await 写法如下:

    async function fn() {
      let res = await request('/xxx');
      request('/yyyy',{res}).then(() => {
        // 对结果进行处理
      })
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    如何使用

    async

    async 关键字写在函数名之前(相当于一个标识),让普通函数具有异步的行为特征,但是整体上代码却是同步执行的。它可以用在以下地方:

    • 函数声明
    • 函数表达式
    • 箭头函数
    • 类的属性方法
    async function fn1() { } //函数声明
    let fn2 = async function () { }//函数表达式
    let fn3 = async () => { }//箭头函数
    let test = {	// 类中属性方法
      async fn4() { }
    }
    
    // 总体同步执行
    async function fn5() {
      console.log(1);
    }
    fn5();// 1
    console.log(2);//2
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    异步函数返回值是一个期约对象,内部逻辑是使用 Promise.resolve方法来处理返回值 。因此可以使用.then方法处理异步任务的结果。

    async function fn(){
      console.log('1.异步函数执行了');
      const res = await '2.插队?不讲武德';
    }
    fn().then(res => console.log(res));
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 如果没有返回值,则 Promise 的状态为解决(fulfilled),结果为 undefined
    async function fn(){
      console.log('异步函数执行了');
    }
    const res = fn();
    console.log(res);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    • 如果有返回值,Promise 的状态为解决(fulfilled),结果为该返回值
    async function fn(){
      console.log('异步函数执行了');
      return '我是返回值';
    }
    const res = fn();
    console.log(res);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在这里插入图片描述

    • 如果在异步函数中抛出错误那么会返回一个失败(rejected)状态的期约
    async function fn(){
      console.log('异步函数执行了');
      throw new Error('出错了');
    }
    const res = fn();
    console.log(res);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在这里插入图片描述

    • 如果返回一个失败(rejected)状态的期约却并不会被异步函数捕获
    async function fn(){
      console.log('异步函数执行了');
      return Promise.reject('出错了');
    }
    const res = fn();
    console.log(res);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在这里插入图片描述

    await

    当遇到 await 后,会先暂停异步函数的执行,让出 JS 运行时线程,执行别的代码。当异步函数拿到结果又会在合适的时机恢复运行。

    async function fn(){
      console.log('1.异步函数执行了');
      const res = await '2.插队?不讲武德';
      console.log(res);
      console.log('3.终于轮到我了')
    }
    fn();
    console.log('4.我先插个队');
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在这里插入图片描述
    注意:await 关键字只会暂停异步函数的执行,并不会影响异步函数之外的代码。如上面的代码执行 fn 函数打印“1.异步函数执行了”,遇到 await 关键字,就暂停函数中 await 后边的代码执行,转而去打印“4.我先插个队”。

    await 使用限制

    早期 await 只允许在异步函数中使用,再同步函数使用会报错。
    在这里插入图片描述
    但是从 ES2022 开始,允许在模块的顶层独立使用await命令。如下所示:
    在这里插入图片描述
    它的主要目的是使用await解决模块异步加载的问题。

    因为 await 等待的是一个异步操作,而异步任务有可能失败(rejected),所以需要把 await 放在 try/catch中。

    function fn() {
      try{
        let res = await ···;
        let res1 = await ···;
        let res2 = await ···;
      } catch(err) {
        // 失败处理逻辑
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    题目分析

    async function async1() {
      console.log("async1 1");
      await async2();
      console.log("async1 2");
      setTimeout(() => {
        console.log('timeout1')
      }, 0)
    }
    async function async2() {
      setTimeout(() => {
        console.log('timeout2')
      }, 0)
      console.log("async2");
    }
    async1();
    setTimeout(() => {
      console.log('timeout3')
    }, 0)
    console.log("start")
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    在这里插入图片描述

    1. 执行async1函数,打印“async1 1”,遇到await,暂停async1函数执行,向消息队列添加一个在async2执行完之后执行的任务;
    2. 执行async2函数,遇到setTimeout,将其添加到宏任务队列,打印 “async2”,async2函数执行完毕,把给 await 提供值的任务添加到消息队列,async1退出;
    3. 执行最外层同步代码,又遇到setTimeout,将其添加到宏任务队列,打印“start”,同步代码执行完毕。
    4. JS 运行时从消息队列中取出给 await 提供值的任务,并将 undefined 赋值给它,并添加一个恢复async1函数执行的任务。
    5. 取出恢复async1函数执行的任务,打印“async1 2”;遇到setTimeout添加到宏任务队列。
    6. 宏任务队列不为空,执行队头宏任务,打印“timeout2”;微任务队列为空;
    7. 宏任务队列不为空,执行队头宏任务,打印“timeout3”;微任务队列为空;
    8. 宏任务队列不为空,执行队头宏任务,打印“timeout1”;微任务队列为空;
    9. 程序结束。

    await 不会等待 setTimeout

    分析上面的代码我发现 await 并没有等待 async2 函数中 setTimeout 函数执行结束,原因是 setTimeout 函数在调用时会同步返回一个随机数,而实际上 await 等待的就是异步任务结果,此时,这个随机数就会被await 认为是 setTimeout 异步任务的结果,就放行了。

    async function fn() {
      await fn1();
      await fn2();
    }
    async function fn1() {
      setTimeout(() => {
        // 省略代码
      }, 200);
    }
    async function fn2() {
      Math.random();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    上面的代码 fn1 和 fn2 函数对于 await 来说是等价的。

    浅谈生成器

    生成器是 ES6 新增的,它拥有在一个函数块内暂停和恢复代码执行的能力。(箭头函数不能用来定义生成器函数)只要调用生成器函数就会产生一个生成器对象,它实现了 Iterator 接口,初始为暂停执行状态,可以调用 next 方法让生成器恢复执行。

    next 方法的返回值类似于迭代器.{value:xxx,done:flase||true}当done为true 就意味着生成器已经执行完毕,此时再调用next方法,value是生成器函数的返回值,默认为 undefined。

    function* fn() {
      yield 1;
      yield 2;
    }
    // 生成生成器对象,暂停状态
    const Fn = fn();
    // 恢复执行
    console.log(Fn.next());//{value: 1, done: false}
    // 恢复执行
    console.log(Fn.next());//{value: 2, done: false}
    // 执行完毕
    console.log(Fn.next());//{value: undefined, done: true}
    // 执行完毕后再调用 next 函数,会输出同样的结果
    console.log(Fn.next());//{value: undefined, done: true}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    yield

    yield 关键字可以让生成器停止执行。生成器函数在遇到 yield 关键字后会暂停执行并保留函数作用域状态,只能通过调用生成器对象的 next 方法恢复执行。并且 yield 语句生成的值就在 next 函数返回值里,此时生成器函数时done:false的状态,通过 return 退出生成器会处于done:true状态

    function* fn() {
      yield 1;
      return 2;
    }
    const Fn = fn();
    // yield 生成的值就是 next 函数返回的对象里的 value
    console.log(Fn.next());//{value: 1, done: false}
    console.log(Fn.next());//{value: 2, done: true}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    生成器作为可迭代对象

    因为生成器对象实现了 Iterator 接口,可以使用 for of 遍历,它会自动调用 next 方法。

    function* fn() {
      yield 1;
      yield 2;
      yield 3;
    }
    for (let item of fn()) {
      console.log(item);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在这里插入图片描述

    next的参数

    生成器对象的 next 方法是可以接收一个参数的,这个参数最终会传给 yield ,注意两点:

    • 第一次next传参是没用的,只有从第二次开始next传参才有用。
      原因:我的理解是第一次调用 next 函数执行的是第一个 yield 之前的代码,这里就是console.log('开始执行');,所以这是传参没有 yield 接收。
    • next传值时,要记住顺序是,先右边yield,后左边接收参数。
      意思就是在第二次调用 next 函数时,会先执行 yield 后边的代码,接着将参数赋值给等号右边的变量。
    function* fn() {
      console.log('开始执行');
      let r1 = yield 1;
      let r2 = yield r1;
      return r2;
    }
    const Fn = fn();
    console.log(Fn.next());//{value: 1, done: false}
    console.log(Fn.next(2));//{value: 2, done: false}
    console.log(Fn.next(3));//{value: 3, done: true}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    提前终止生成器

    return

    return 会强制生成器进入关闭状态,不可逆,最后的状态值就是传给return()的参数。并且调用return以后再调用next方法都会返回{value:undefined,done:true}

    function* fn() {
      yield 1;
      return 2;
      yield 3;
    }
    const Fn = fn();
    console.log(Fn.next());//{value: 1, done: false}
    console.log(Fn.next());//{value: 2, done: true}
    console.log(Fn.next());//{value: undefined, done: true}
    console.log(Fn.next());//{value: undefined, done: true}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    throw

    throw 方法会在暂停的时候将一个错误注入到生成器对象中,如果错误未被处理,生成器就会关闭

    function* fn() {
      yield 1;
      yield 2;
      yield 3;
    }
    const Fn = fn();
    console.log(Fn.next());//{value: 1, done: false}
    Fn.throw('出错了···')
    console.log(Fn.next());
    console.log(Fn.next());
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在这里插入图片描述

    如果生成器函数内部处理了这个错误,只会跳过对应的yield,可再次恢复执行。

    function* fn() {
      try {
        yield 1;
      } catch (error) {
        console.log('捕获到错误内容:'+ error)
      }
      yield 2;
      yield 3;
    }
    const Fn = fn();
    console.log(Fn.next());//{value: 1, done: false}
    Fn.throw('出错了···');
    console.log(Fn.next());//{value: 3, done: false}
    console.log(Fn.next());//{value: undefined, done: true}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    在这里插入图片描述

    生成器与 async/await

    还记得在介绍 async/await 时说过的需求吗?请求一的结果是请求二的参数,最后拿到结果,下面利用生成器和 Promise 来模拟,注意这样的代码就类似于 async/await 是同步的逻辑

    function fn(x) {
      return new Promise((resolve,reject) => {
        setTimeout(() => {
          resolve(x + 1);
        }, 1000);
      })
    }
    function* fn1() {
      console.log('开始执行');
      let r1 = yield fn(1);
      console.log('请求一完成');// 1s后打印
      let r2 = yield fn(r1);
      console.log('请求二完成');// 2s后打印
      return r2;
    }
    const gen = fn1();
    gen.next().value.then((res) => {
      gen.next(res).value.then(res => {
          console.log('输出结果', gen.next(res));
      })
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    简单实现 async/await

    上面的代码有很多缺陷,只能执行有限步···,现在我们可以稍稍封装一下

    function fn(x) {
      return new Promise((resolve,reject) => {
        setTimeout(() => {
          resolve(x + 1);
        }, 1000);
      })
    }
    function* fn1() {
      console.log('开始执行');
      let r1 = yield fn(1);
      console.log('请求一完成');
      let r2 = yield fn(r1);
      console.log('请求二完成');
      return r2;
    }
    function myAsync (fn) {
      const Fn = fn.apply(this, arguments);//拿到生成器对象
      return new Promise((resolve,reject) => {
        function forward(key,val) {
          let res = null; 
          try{
            res = Fn[key](val);//恢复执行
          } catch(err) {
            return reject(err);
          }
          let {value,done} = res;
          if (done) {//代码执行完了,返回一个解决状态的期约
            return resolve(value);
          } else {
            return Promise.resolve(value).then(value => forward('next',value),err=>forward('throw',err));
          }
        }
        forward('next');//第一次执行
      })
    }
    const asyncFn = myAsync(fn1)
    asyncFn.then(res => console.log(res))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    这样 fn1 函数中 yield 就相当于 await ,且异步函数 myAsync 执行完就能得到一个期约,并且无论 fn1 函数中有多少 yield 都能正确执行,并得到结果。

    总结

    async/await 和 生成的使用细节以及具体的应用还有很多,我在这里只是稍稍总结一下,本篇博客内容也是我的学习笔记吧。我是孤城浪人,一名正在前端路上摸爬滚打的菜鸟,欢迎你的关注。

  • 相关阅读:
    【一起学Rust | 进阶篇 | reqwest库】纯 Rust 编写的 HTTP 客户端——reqwest
    Mac系统下TestCafe初体验
    【Java SE】如何解读Java的继承和多态的特性?
    FPGA片内ROM读写测试实验
    java毕业设计项目struts2的员工工资管理系统|考勤[包运行成功]
    MyBatis if标签:条件判断
    Gartner公布《2023中国ICT技术成熟度曲线》,得帆信息入选低代码代表厂商
    Springboot整合Websocket
    百度智能云服务网格产品 CSM 发布 | 火热公测中
    Java 面试题 —— TCP 粘包、拆包问题
  • 原文地址:https://blog.csdn.net/weixin_46015333/article/details/127398678