美文网首页前端开发那些事儿让前端飞
浅谈Generator函数的异步应用之async函数

浅谈Generator函数的异步应用之async函数

作者: 竹叶寨少主 | 来源:发表于2021-04-26 18:47 被阅读0次
    1.异步编程的终极解决方案

        前文结尾时提到,async/await是异步编程的'终极'解决方案,而终极二字就体现在,使用async/await来操作异步无论是逻辑上还是语义上都与同步操作无限接近(当然只是形式上像,没有改变异步的本质,后面会解释)。
        先来看一下之前使用Generator函数控制异步流程的代码

    function* gen() {
      const res1 = yield promisify_readFile("./text1.txt");
      console.log(res1.toString());
      const res2 = yield promisify_readFile("./text2.txt");
      console.log(res2.toString());
    }
    co(gen);
    

        下面使用async/await实现

    async function asyncReadFile() {
      const res1 = await promisify_readFile("./text1.txt");
      console.log(res1.toString());
      const res2 = await promisify_readFile("./text2.txt");
      console.log(res2.toString());
    }
    asyncReadFile()
    

        可以看到,从形式上看使用async/await进行异步流程处理无需执行器,函数可以像普通函数一样执行,这意味着async函数内置了Generator函数的执行器。从语义上看,async关键字表示函数内部有异步操作,await关键字表示要等待异步操作执行完毕,相比于Generator函数用*声明以及yield表达式划分状态要更加友好。
        下面具体介绍async函数和await关键字的特点。

    2.async函数和await关键字的特点
    2.1 async函数返回值

        async函数返回的是Promise对象,因此可以为async函数指定then,catch等方法。

    asyncReadFile().then(() => {
      console.log("end");
    });
    

        既然async函数返回的是Promise对象,那其结果和状态由什么决定呢

    • 当async函数内部的return有返回值时,该参数会成为then方法成功回调的参数(即Promise的结果值),状态变为成功。
    const promisifyTimeOut = () => {
      return new Promise((resolve) => {
        setTimeout(() => {
          resolve('timeOut')
        }, 500);
      })
    }
    const asyncTimeOut = async () => {
      const res = await promisifyTimeOut()
      return res
    };
    asyncTimeOut().then(
      (res) => {
        console.log('success' + res);
      },
      (r) => {
        console.log('err' + r);
      }
    );
    //success timeOut
    
    • 当async函数内部抛出错误时,状态会立即变为失败,并执行then方法的失败回调或catch方法。
    const asyncTimeOut = async () => {
      const res = await promisifyTimeOut()
      throw res
    };
    asyncTimeOut().then(
      (res) => {
        console.log('success' + res);
      },
      (r) => {
        console.log('err' + r);
      }
    );
    // err timeOut
    

        利用这一点可以,我们可以进行对async函数的错误处理,后面会介绍。

    2.2 await关键字的特点
    • await命令只能用在async函数之中,用在普通函数中会报错。
    • await命令后面如果是一个 Promise 对象,返回该Promise 对象的结果值,如果不是 Promise 对象,就直接返回对应的值 。
    (async function(){
        const res1 = await Promise.resolve('foo')
        console.log(res1)
        const res2 = await 'bar'
        console.log(res2)
    })()
    // foo
    // bar
    
    3.async函数的错误处理

        前面提到,async函数内部抛出错误时,其状态会立即变为失败并执行失败回调(假设指定了失败回调)。因此任何一个await关键字后面的Promise状态变为rejected都会导致async函数立即中断执行。

    const promisifyTimeOut = () => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          reject("some err");
        }, 500);
      });
    };
    const asyncTimeOut = async () => {
      await promisifyTimeOut();
      console.log("foo");
    };
    asyncTimeOut().catch((r) => console.log(r));
    // some err
    

        上面代码,await后的异步抛出错误,async函数中断执行导致foo没有被打印。如果不想让async函数内部一抛出错误就终止执行,可以将可能抛出错误的Promise包在try...catch代码块中 ,或者为可能抛出错误的Promise指定失败回调(指定then方法或catch方法),下面以try...catch为例演示。

    const asyncTimeOut = async () => {
      try {
        await promisifyTimeOut()
      } catch (error) {
        console.log(error)
      }
      console.log("foo");
    };
    asyncTimeOut().catch((r) => console.log(r))
    // some err
    // foo
    

        如果使用上述两种方法进行错误处理,则async函数指定的失败回调将不生效(假设不在catch语句或Promise失败回调中将错误抛出)。另外,多个await语句可以一起包在try...catch中进行统一错误处理。

    4.async函数的实现原理

        其实,经过前面对co模块的讨论,以及上面对async函数特点的介绍,我们可以知道,async/await就是Generator函数的语法糖,我们只需根据其特点进行封装,具体如下。

    • async函数内置Generator函数执行器。
    • async函数返回Promise,要等内部所有Promise执行完后再改变状态,函数内部抛出错误,状态立即变为rejected。

        我们假设async的内置执行器叫做spawn函数,那么async函数的结构就是这样的

    const async = (gen) => {
      return () => {
        return spawn(gen);
      };
    };
    

        接下来实现执行器,其原理与前面讨论的co模块基本一致

    function spawn(genF) {
      return new Promise(function (resolve, reject) {
        const gen = genF();
        function step(data) {
          let res;
          try {
            res = gen.next(data);
          } catch (e) {
            // 内部抛出错误 状态变为rejet
            return reject(e);
          }
          if (res.done) {
            return resolve(res.value);
          }
          // 为异步指定成功/失败回调 成功则继续执行 失败则立即rejected
          Promise.resolve(res.value).then(step, (r) => reject(r));
        }
        step();
      });
    }
    

    下面简单测试一下

    const promisify = (data) => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve(data);
        }, 300);
      });
    };
    
    function* testGen() {
      const res1 = yield promisify(1);
      console.log(res1);
      const res2 = yield promisify(2);
      console.log(res2);
      return res2;
    }
    
    const async = (gen) => {
      return () => {
        return spawn(gen);
      };
    };
    // 得到async函数
    const asyncFoo = async(testGen);
    // 得到async函数的执行结果
    const res = asyncFoo();
    setTimeout(() => {
      console.log(res);
    }, 1000);
    // 1
    // 2
    // Promise { 2 }
    
    5.async函数与执行环境栈

        在前面对JavaScript执行上下文的讨论时我们知道,JavaScript引擎在执行代码之前, 会创建一个执行环境栈,之后创建全局执行上下文并将它压入栈中作为栈底。每遇到一个函数执行时,都会为该函数创建执行上下文,并将其推入执行环境栈中,形成一个由执行上下文构成的堆栈(context stack)。每个上下文都有一个与之相关联的变量对象,包含了当前上下文的变量,函数,形参等。栈是“后进先出”的数据结构,因此最后产生的上下文环境首先执行完成并出栈,然后再执行它下层的上下文,栈底永远是全局上下文,当浏览器窗口关闭,全局上下文才会出栈。
        Generator函数不是这样,执行Generator函数产生的上下文,遇到yield命令时,会暂时退出堆栈,但是并不消失,变量对象里面的所有变量和对象会冻结在当前状态。等到执行next命令时,执行上下文会重新加入执行环境栈,冻结的变量和对象恢复执行。而async函数是Generator函数的语法糖,因此他也有一样的特性,即async 函数可以保留运行堆栈。
        下面用一个例子进行对比说明

    const timeOut = () => {
      return new Promise((resolve) => {
        setTimeout(() => {
          resolve();
        }, 500);
      });
    };
    (function () {
      for (let i = 0; i < 3; i++) {
        timeOut().then(() => {
          console.log(i);
        });
      }
      console.log("end");
    })();
    // end
    // 0 1 2
    

        上面代码会先打印end 之后012几乎同时打印,原因不难分析,由于promise.then方法不会将当前上下文冻结,因此循环的进行不受影响,而因为then方法中的回调会异步执行,因此三个log语句会几乎同时被加入任务队列,最终造成上述的执行结果。
        下面用async/await重写上面代码

    (async function () {
      for (let i = 0; i < 3; i++) {
        await timeOut();
        console.log(i);
      }
      console.log("end");
    })();
    // 0 1 2 end
    

        上面代码会依次打印0 1 2 end。
        分析原因,由于async 函数可以保留当前上下文环境,当遇到await命令,当前上下文的所有状态都被冻结,包括for循环在内的所有代码都会暂停执行,因此造成上述执行结果。
        其实,这条特性可以理解为, await命令后面的所有代码都会进入异步任务队列。 await相当于then的语法糖,其后面的代码都进入了promise.then的回调函数中,会进入任务队列异步执行。
        利用这一点,我们可以实现休眠器。

    function sleep(interval) {
      return new Promise((resolve) => {
        setTimeout(resolve, interval);
      });
    }
    // 用法
    async function Async(timeOut) {
      await sleep(timeOut);
      console.log("foo!");
    }
    Async(1000);
    // 一秒后打印foo!
    

        关于上述特性,有两点需要说明

        1.await语句冻结的只是async函数的上下文,即async函数后面的代码执行不会被阻塞。这也就说明,async/await只是写起来像同步代码,异步的本质没有改变。

    async function Async(timeOut) {
      await sleep(timeOut);
      console.log("foo!");
    }
    Async(0)
    console.log('end!')
    // end!
    // foo!
    

        2. 上面说到,遇到await关键字,其后所有代码都将被冻结,因此await语句下面的异步任务也会等到await语句的异步结束后再执行。这点对于具有依赖关系的异步(继发关系)的处理是非常友好的。但同样的,如果两个异步没有继发关系,则尽量不要这么写,因为会造成阻塞。可以使用Promise.all()等方式让他们并发执行,而不是继发执行。

    function sleep(interval) {
      return new Promise((resolve) => {
        setTimeout(() => {
          console.log("foo!");
          resolve();
        }, interval);
      });
    }
    async function Async() {
      await Promise.all([sleep(500), sleep(500)]);
      console.log("end");
    }
    Async();
    // 两个异步并发执行 几乎同时打印foo!
    

    相关文章

      网友评论

        本文标题:浅谈Generator函数的异步应用之async函数

        本文链接:https://www.haomeiwen.com/subject/fadxlltx.html