ES8 中的 async/await —— 异步函数

1,222 阅读7分钟

异步函数

异步函数,也称为 async/await (语法关键字),是 ES8 规范新增的,是 ES6PromiseECMAScript 函数 中的应用。

为什么需要 async/await ?

ES8 的 async/await 主要是为了 解决利用异步结构组织代码的问题.

举个最简单的例子,下面的 Promise 超时后会进行 resolve,提供了 4 种写法:

   // 写法 1
   new Promise((resolve)=>{
      setTimeout(()=>{
        resolve('解决了');
      },1000);
    }).then((value)=>{
      console.log(value);
    });
    
   // 写法 2
   new Promise((resolve)=>{
      setTimeout(resolve, 1000, '解决了');
    }).then((value)=>{
      console.log(value);
    });
    
   // 写法 3
   function handler(value) {
      console.log(value);
   }
   new Promise((resolve)=> setTimeout(resolve, 1000, '解决了')).then(handler);
   
   // 写法 4
   function handler(value) {
      console.log(value);
   }
   let p = new Promise((resolve)=> setTimeout(resolve, 1000, '解决了'));
   p.then(handler);

上述的 4 种写法,后一种都是前一种的优化改进,但是仔细观察发现,其实每一种写法的改进其实都不大. 因为任何需要访问这个 Promise 所产生值的代码,都需要以处理程序 (then + cllback) 的形式来接收这个值. 而上面的只是一个简单的需求,还没有其他多余的逻辑,仍旧显得比较复杂,可想而知如果你的使用场景很复杂,堆积组合的代码将会变得非常不直观.

相比于上面,下面的写法显然要更容易、更直观的被理解:

    async function handler() {
      let rs = await new Promise((resolve) => setTimeout(resolve, 1000, '解决了'));
      console.log('inner = ',rs);
    }
    handler();

async 关键字

async 关键字用于 声明异步函数,可以用在 函数声明、函数表达式、箭头函数方法 上.

async function example() {} // 函数声明

let example = async function() {}; // 函数表达式

let example = async () => {}; // 箭头函数

class example {
   async exampleHandler() {} // 方法
}
  • async 关键字可以让函数具有 异步特征,但总体上其代码仍然是同步求值的.
    • 异步函数的 返回值(没有显示的 return 就会返回 undefined) 默认是一个被 Promise.resolve() 包裹的 Promise 对象,当然直接返回一个 Promise 对象也是一样的.
      // 下面的数字代表输出顺序  
      async function example() {
        console.log("example"); // 1. example
      }
      let rs = example();
      console.log('main'); // 2. main
      console.log(rs); // 3. Promise {<fulfilled>: undefined}
      rs.then(console.log); // 4. undefined
    
    • 异步函数的 返回值 期待一个实现 thenable 接口的对象,但这并不是严格要求的。如果返回的是 实现 thenable 接口 的对象,则这个对象可以由提供给 then() 的处理程序 “解包”。如果不是,则返回值就被当作状态为 fulfilledPromise.

    PS:实现 thenable 接口的对象,简单理解就是一个包含 then 方法的对象。“解包” 这里可以理解为把实现了 thenable 接口的对象中的 then 方法,当作在实例化 Promise 时要传入的 executor,如:new Promise(executor).

     // 1. 返回原始值
     async function example() {
        return 'example';
     }
     example().then(console.log); // example
    
    // 2. 返回复杂类型(且没有实现 thenable 接口)
    async function example() {
       return ['example'];
    }
    example().then(console.log); // ['example']
    
    // 3. 返回一个实现了 thenable 接口的非 Promise 对象
      let obj = {
        then(resolve, reject) {
          console.log("obj.then run...");
          resolve('解决了');
        }
      }
      async function example() {
        return obj;
      }
      example().then(console.log);
      // 输出顺序:obj.then run...   解决了
    
    • 与在 Promise 中一样,在 异步函数抛出错误 会返回状态为 rejectedPromise. 同样的,外部的 try/catch 无法进行捕获.
      // 1. 普通函数 throw Error
      function example() {
        console.log('example run ...'); // 1. example run ...
        throw Error('出错了');
      }
      
      try {
        example();
      } catch (error) {
        console.log('error = ', error);
        // 2. error =  Error: 出错了
       //          at example (index.html:41)
       //          at index.html:45
      }
      
      // 2. 异步函数 throw Error
      async function example() {
        console.log('example run ...'); // 1. example run ...
        throw Error('出错了');
      }
    
      try {
        // 用 Promise 上的 then 方法
        example().then(undefined, (reason) => {
          console.log('then onReject = ', reason);
          // 2. then onReject =  Error: 出错了
         //       at example (index.html:41)
        //        at index.html:45
        });
        
        // 用 Promise 上的 catch 方法
        // example().catch(console.log);
        
      } catch (error) {
       // 和 Promise 一样,内部抛出的异常会被 reject 处理,并不会被 try/catch 捕获
        console.log('error = ', error); // 不会被执行
      }
    

await 关键字

因为 异步函数 主要针对不会马上完成的任务,所以需要一种 暂停 和 恢复 执行的能力。使用 await 关键字可以暂停异步函数代码的执行,等待 Promise 进入 settled 状态。

settled 状态,即 Promise 状态变更为 fulfilledrejected.

  • await 关键字会暂停执行 异步函数 后面的代码,让出 JavaScript 运行时 的执行线程,这一点与 生成器函数(generator function) 中的 yield 关键字是一致的.
  • await 关键字会尝试 “解包” 对象的值,然后将这个值传给表达式,再异步恢复异步函数的执行.

“解包” 这里可以理解为把 await 后面的 Promisesettled 状态下的 valuereason 进行返回.

  async function handler() {
      let rs = await new Promise((resolve) => setTimeout(resolve, 1000, '解决了'));
      console.log(rs); // 大约 1s 后输出:'解决了'
  }
  handler();
  • await 关键字期望一个实现 thenable 接口的对象,这和 async 的返回值一样不是严格要求。如果是实现 thenable 接口的对象,则这个对象可以由 await“解包”。如果不是,则这个值就被包装成状态为 fulfilled 状态的 Promise.
    // 1. await + 原始值
    async function example() {
      console.log(await 'example'); // example
    }
    example();

    // 2. await + 返回复杂类型(且没有实现 thenable 接口)
    async function example() {
      console.log(await ['example']); // ['example']
    }
    example();
    
    // 3. await + 实现了 thenable 接口的非 Promise 对象
    let obj = {
      then(resolve, reject) {
        resolve('解决了');
      }
    }
    async function example() {
      console.log(await obj); // 解决了
    }
    example();
    
   // 4. await + Promise 对象
    async function example() {
      console.log(await Promise.resolve('解决了')); // 解决了
    }
    example();
  • await 关键字在等待会 抛出错误同步操作,会返回状态为 rejectedPromise. 和 async 关键字一样,外部的 try/catch 无法进行捕获.
    // 定义函数
    async function example() {
      console.log('start throw error'); // 1. start throw error
      await (() => {
        throw Error('抛出异常了');
      })();
      console.log('end throw error'); // 不会输出,因为 await 后抛出了异常,直接向外返回了 rejected 状态的 Promise 
    }
    
    // 执行函数
    try {
      example().catch(error => {
         console.log("promise catch = ",error); // 2. promise catch =  Error: 抛出异常了
      });
    } catch (error) {
      console.log("try catch = ", error); // 不会输出,因为无法捕获
    }
  • await 关键字必须在 异步函数 中使用,不能在顶级上下文如:<script> 标签或 模块 中使用.

async/await 暂停和恢复执行

async/await 中真正起作用的是 await,可以把 async 关键字简单的当作一个 标识符. 因为如果 异步函数 中不使用 await 关键字,其执行基本上跟普通函数没有什么区别,但是对于 异步函数 的返回值还是会和 普通函数 有区别,这一点在上面有说明.

    async function example1() {
      console.log(await new Promise((resolve, _) => {
        setTimeout(resolve('example1'),0);
      }));
    }

    async function example2() {
      console.log(await 'example2');
    }

    async function example3() {
      console.log('example3');
    }

    example1();
    example2();
    example3();
    // 输出顺序:example3 example1 example2

简单理解上面的输出顺序:

  • JavaScript 运行时在碰到 await 关键字时,会记录在哪里暂停执行.
  • 等到 await 右边的值可用了,JavaScript 运行时会向 消息队列 中推送一个任务,这个任务会 恢复异步函数的执行.
  • 并且会按在 消息队列 中的顺序,依次恢复执行.

因此,即使 await 后面跟着一个 立即可用的值,函数的其余部分也会被 异步求值.

下面举个例子,详细介绍一下具体的过程:

    async function async1() {
      console.log(2);
      console.log(await Promise.resolve(8));
      console.log(9);
    }

    async function async2() {
      console.log(4);
      console.log(await 6);
      console.log(7);
    }
    
    console.log(1);
    async1();
    console.log(3);
    async2();
    console.log(5);
    
// 输出顺序: 
    1
    2
    3
    4
    5
    8
    9
    6
    7

一起来分析下执行过程:

(1) 执行 console.log(1),输出 1

(2) 执行 async1() 异步函数:

  • (2.1) 执行 console.log(2),输出 2
  • (2.2) 遇到 console.log(await Promise.resolve(8)),此时碰到了 await 关键字先暂停执行,向消息队列中添加一个 Promise 在 settle 之后且值为 8 的任务 (3) 此时 async1() 函数先退出执行

(4) 执行 console.log(3),输出 3

(5) 执行 async2() 异步函数:

  • (5.1) 执行 console.log(4),输出 4
  • (5.2) 遇到 console.log(await 6),从上面对于 await 的介绍中可以知道,它等价于 console.log(await Promise.resolve(6)),此时碰到了 await 关键字先暂停执行,向消息队列中添加立即可用值为 6 的任务

(6) 此时 async2() 函数先退出执行

(7) 执行 console.log(5),输出 5

(8) 到这,主线程已经执行完毕

(9) JavaScript 运行时从消息队列中取出解决 await 后面 Promise 的处理程序

(10) JavaScript 运行时从消息队列中取出恢复执行 async1()的任务及值 8

  • (10.1) 此时 console.log(await Promise.resolve(8)) 等价于 console.log(8),输出 8
  • (10.2) 执行 console.log(9),输出 9
  • (10.3) 此时 async1 执行完成并返回

(11) JavaScript 运行时从消息队列中取出恢复执行 async2()的任务及值 6

  • (11.1) 此时 console.log(await 6) 等价于 console.log(6),输出 6
  • (11.2) 执行 console.log(7),输出 7
  • (11.3) 此时 async2 执行完成并返回 其实也可以和 事件循环 结合在一起看,下面给出了简单的图解: