相关推荐recommended
重学JavaScript高级(十二):asyncawait-事件循环-面试高频
作者:mmseoamin日期:2024-01-19

async/await-事件循环

前面我们学习了生成器和迭代器,那么在本篇文章中,我们主要讲解生成器与Promise的结合使用,从而引出async/await语法,同时会涉及面试中频次最高的一个知识点:事件循环

生成器与异步处理

  • 首先需要了解回调地狱
    • 在Promise出来之前,我们多次请求网络接口,有可能产生回调地狱
      //伪代码
      function request(url) {
        //请求的逻辑代码
        //返回一个结果
        return res;
      }
      //这样一层嵌套着一层,就是回调地狱
      request("第一次").then((res1) => {
        request("第二次" + res1).then((res2) => {
          request("第三次" + res2);
        });
      });
      
      • 而Promise的出现就解决了回调地狱的问题
        • 不明白return的返回值的,可以看我先前的文章,Promise–详解
          function request(url) {
            return new Promise((resolve, reject) => {
              resolve(url);
            });
          }
          //这样就解决了回调地狱的问题,将代码写成了链式调用
          request("第一次")
            .then((res1) => {
              console.log(res1);
              return request("第二次" + res1);
            })
            .then((res2) => {
              console.log(res2);
              return request("第三次" + res2);
            })
            .then((res3) => {
              console.log(res3);
              return request("结束");
            });
          
          • 以上代码虽然解决了回调地狱的问题,但是看上去不是很优雅
          • 因此可以和生成器相结合,优化出以下的代码
            /*理想代码
            function getData(){
              let res = yield request(url);
              let res1 = yield request(res);
              let res2 = request(res1);
            }
            */
            function request(url) {
              return new Promise((resolve, reject) => {
                resolve(url);
              });
            }
            //在获取上一次结果之后,再进行下一次请求
            //因此我们就可以应用到yield
            function* getdata(url) {
              //yield空格后面的函数会直接执行
              let res = yield request(url);
              let res1 = yield request(res);
              let res2 = yield request(res1);
              console.log("请求结束");
            }
            //执行生成器函数
            let generator = getdata("test");
            //这时候执行next,其返回值{value:Promise,done{false}}
            //因此我们获取value调用then方法,就可以获取resolve的值
            // console.log(generator.next());
            generator.next().value.then((res) => {
              console.log(res);
              //第一次执行完之后,就会去进行下一次请求
              generator.next(res + "test").value.then((res1) => {
                console.log(res1);
                //第二次执行完之后,就会进行第三次
                generator.next(res1 + "test").value.then((res2) => {
                  console.log(res2);
                  //此时运行最后一行console代码
                  generator.next();
                });
              });
            });
            
            • 而我们只希望有 getdata这种函数,所以引出了 async/await的语法
            • 实际上是Promise与生成器的语法糖
              function request(url) {
                return new Promise((resolve, reject) => {
                  setTimeout(() => {
                    resolve(url);
                  }, 2000);
                });
              }
              //在获取上一次结果之后,再进行下一次请求
              //因此我们就可以应用到yield
              async function getdata(url) {
                //yield空格后面的函数会直接执行
                let res = await request(url + 1);
                console.log(res);
                let res1 = await request(res + 2);
                console.log(res1);
                let res2 = await request(res1 + 3);
                console.log(res2);
                console.log("请求结束");
              }
              getdata("test");
              

              异步函数async/await

              异步函数结构

              函数分为:普通函数、生成器函数、异步函数,本次就开始学习异步函数

              • 在函数前面加 async即可,并当成普通函数执行
                async function foo(){}
                const bar = async ()=>{}
                

                异步函数的返回值

                异步函数内部代码执行过程和普通函数是一致的

                • 普通函数没有指定返回值的时候,返回的是undefined;而异步函数返回值相当于被Promise.resolve()包裹
                • 如果返回的是一个Promise,则会由这个Promise决定
                • 如果返回的是一个对象,且对象中有then方法,则会由这个then方法决定
                  //其他情况之前的文章有讲过,不再演示
                  async function foo() {
                    return 123;
                  }
                  foo().then((res) => {
                    console.log(res);//123
                  });
                  

                  异步函数的异常

                  异步函数返回的是一个Promise,Promise有三种状态,正常执行是pending,return是fulfilled,那么何时抛出异常

                  • 一些api使用错误时
                  • 主动通过throw抛出
                    async function foo() {
                      //在执行的时候,不会立即报错,会将错误信息用reject包裹
                      "123".filter();
                        //或者使用throw进行抛出异常
                        throw new Error("主动抛出异常")
                      return 123;
                    }
                    foo()
                      .then((res) => {
                        console.log(res); //123
                      })
                      .catch((err) => {
                        console.log(err);
                        //不会影响下面的代码执行
                        console.log(123456);
                      });
                    

                    await关键字的使用

                    • 必须在异步函数中使用
                    • 一般后面跟着一个表达式,且这个表达式返回的是一个Promise(await后面可以直接跟一个普通函数或者数据,但是没有任何意义)
                    • await会等到 Promise状态为fulfilled的时候,继续执行下面的代码
                    • 若Promise返回的是rejected,就需要进行捕获
                      function request(count) {
                        return new Promise((resolve, reject) => {
                          setTimeout(() => {
                            resolve(count);
                          }, 1000);
                        });
                      }
                      async function getData() {
                        let res = await request(1);
                        console.log(res);
                        let res1 = await request(res + 1);
                        console.log(res1);
                        let res2 = await request(res1 + 1);
                        console.log(res2);
                      }
                      getData();
                      
                      • 捕获异常的方式
                        function request(count) {
                          return new Promise((resolve, reject) => {
                            setTimeout(() => {
                              reject(count);
                            }, 1000);
                          });
                        }
                        //方式一
                        async function getData() {
                          let res = await request(1);
                          console.log(res);
                          let res1 = await request(res + 1);
                          console.log(res1);
                          let res2 = await request(res1 + 1);
                          console.log(res2);
                        }
                        //因为异步函数的返回值是Promise
                        getData().catch((err)=>{
                            console.log(err)
                        })
                        //方式二,使用try catch
                        async function getData() {
                          try {
                            let res = await request(1);
                            console.log(res);
                            let res1 = await request(res + 1);
                            console.log(res1);
                            let res2 = await request(res1 + 1);
                            console.log(res2);
                          } catch (error) {
                            console.log(error);
                          }
                        }
                        

                        await/async关键字的结合使用

                        • 多个异步函数之间可以使用await
                          async function first(){}
                          async function second(){}
                          async function third(){}
                          async function foo(){
                              //第一个函数执行完成,且返回的是fulfilled状态,才会执行下面的代码
                              await first()
                              await second()
                              await third()
                          }
                          foo()
                          

                          进程和线程

                          • 进程和线程是操作系统的两个概念
                            • 进程:计算机已经运行的程序(打开应用程序,就会开启至少一个进程),是操作系统管理程序的一种方式
                            • 线程:操作系统能够运行 运算的调度的最小单位,通常情况下它被包含在进程中
                            • 以下是形象的解释
                              • 进程:可以认为,打开一个应用程序,就会默认启动一个进程(可以是多进程)
                              • 线程,每一个进程中,都会至少启动一个线程来执行其中的代码(也有可能是多线程)
                              • 所以可以说,进程是线程的容器

                                浏览器中的JS线程

                                • JS是单线程(可以开启workers),但是JS的线程有自己的 容器:浏览器和Node

                                • 浏览器是多进程的,当我们打开 一个新的tab页面的时候,就会开启一个新的进程,这是为了防止一个页面卡死,而造成所有页面无法响应

                                  • 每个进程会有多个线程,其中有一个线程是专门分给JS代码运行的

                                  • 即,JS代码一次只能做一次事情,如果这个线程十分耗时,当前线程 就会阻塞

                                  • 所以真正耗时的操作,并不是由JS线程在执行的

                                    • 浏览器每个进程都是多线程的,那么其他线程可以来完成这个耗时的操作
                                    • 比如网络请求、定时器,我们只需要在特定的时候执行应该有的回调即可

                                      浏览器的事件循环

                                      • 首先了解一下事件队列
                                        • 浏览器在处理DOM监听,ajax请求,定时器的时候,会将相关函数里面的回调函数,放入事件队列中
                                        • 事件队列是一种数据结构,遵循先进先出的原则
                                        • 那么在JS执行的过程中,遇见了定时器等相关事件,又会怎么操作
                                          • 首先会执行函数,但是不会执行里面的回调函数
                                          • 将里面的回调函数交由浏览器其他线程
                                          • 其他线程在处理好之后,将回调函数放入事件队列中
                                          • 待执行栈中空了,就会从事件队列中取出待执行的函数

                                            重学JavaScript高级(十二):asyncawait-事件循环-面试高频,image.png,第1张

                                            微任务和宏任务

                                            事件循环中并非只维护者一个队列,事实上有两个队列:宏任务队列和微任务队列

                                            • 宏任务队列:ajax、setTimeout、setInterval、DOM监听、Rendering等

                                            • **微任务队列:**Promise的then回调(Promise中的代码会立即执行),Mutation Observer API queueMicrotask()等

                                            • 那么这两个队列的优先级是什么样的

                                              • main script中的代码优先执行
                                              • 再执行 任何一个宏任务之前(不是队列,是单个的宏任务),都会先去检查微任务队列中,是否有任务需要执行
                                                • 也就是说 每一个宏任务执行之前,必须保证微任务的队列是空的
                                                • 如果不为空,那么就优先执行微任务队列中的任务
                                                • 若微任务中的代码不断往微任务队列中增加任务,则宏任务中的代码则会无线延期

                                                  抛出异常-throw

                                                  遇到throw,代码就会停止执行,且后面的代码不会执行

                                                  • throw后面可以跟String、number、boolean、对象
                                                    throw {message:"错误",code:-1001}
                                                    
                                                    • 后面可以接表达式(new Error)
                                                      throw new Error("这是错误")
                                                      //会把错误地方的调用栈,错误信息都会打印出来
                                                      

                                                      捕获异常的方式

                                                      当代码抛出异常之后,不去捕获的话,就会很危险

                                                      try catch捕获异常