async/await原理实现

695 阅读5分钟

前言

async/await是使用同步写法实现异步执行的语法糖。语法糖就是用简单的写法表达复杂的逻辑,所以要能够知道为什么这么写,就要搞明白它背后代表的复杂逻辑是什么。

本文参考了这篇大神的文章,是我自己学习之后按照自己的理解整理的一篇学习笔记。

# 7张图,20分钟就能搞定的async/await原理!为什么要拖那么久?

同步执行、异步执行和同步写法

首先,做一下概念辨析。

同步执行:代码是串行执行的,程序是阻塞的。

异步执行:代码是非串行执行的,程序是非阻塞的。

同步写法:代码是串行执行的,不考虑程序是否阻塞。

一个效果,两个特性

async/await实现的效果是同步写法实现异步执行,对应的具体要求有两点:

  1. 代码是串行执行的
  2. 程序是非阻塞的

为了实现这个效果,async/await有两个特性:

  1. async函数返回一个Promise
  2. await后面得是一个Promise,如果不是会自动转成Promise
// 模拟接口请求
function fn(num) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(num * 2);
    }, 1000);
  });
}

// 使用async/await构建业务逻辑
async function async() {
  const num1 = await fn(1);
  const num2 = await fn(num1);
  const num3 = await fn(num2);
  return num3;
}

在上述案例中,fn是异步代码,如果串行执行只能等待,但实际上程序又是非阻塞的,那么唯一的等待方式就是跳出async函数,先执行外面的逻辑,等待fn函数执行完了再回来继续执行。

生成器的秘密

能够跳出再回来的函数,就是生成器函数。

async/await语法糖的底层就是生成器。

生成器有两个特性:1. yield 跳出执行 2. next 继续执行

把上面的案例中的await全部换成yield就可以得到一个生成器函数:

// 使用生成器构建业务逻辑
function* gen() {
  const num1 = yield fn(1);
  const num2 = yield fn(num1);
  const num3 = yield fn(num2);
  return num3;
}

下面我们就用生成器实现async/await的效果:1. 代码是串行执行的 2. 程序是非阻塞的。


const g = gen(); // 构建生成器
let res = null;
res = g.next(); // next()激活生成器,从头开始执行,执行到yield fn(1)跳出,返回fn(1)的结果
res = g.next(res.value); // next(res.value)激活生成器,从const num1 = res.value 开始执行,执行到yield fn(num1)跳出,返回fn(num1)的结果
res = g.next(res.value); // next(res.value)激活生成器,从const num2 = res.value 开始执行,执行到yield fn(num2)跳出,返回fn(num2)的结果
res = g.next(res.value); // next(res.value)激活生成器,从const num3 = res.value 开始执行,执行到return num3结束,返回num3

这里展示了生成器的串行执行过程,接下来我们把异步放进去,实现非阻塞执行。

这里fn()会返回一个Promise,所以只需要在Promise.then()中执行g.next()就可以了。

const g = gen(); // 构建生成器
let res = null;
res = g.next(); // next()激活生成器,从头开始执行,执行到yield fn(1),返回fn(1)的结果
res.value.then((data) => (res = g.next(data))); // 等待fn(1)返回的Promise执行完成后再next(data)激活生成器,本质就是await Promise()的过程

Promise.then中激活生成器,就是await Promise()的过程,就是async/await的第二个特性:

await后面得是一个Promise,如果不是会自动转成Promise

这里是单个Promise的情况,如果要实现多个Promise依序等待和激活,就要用到递归了。

const g = gen(); // 构建生成器
let res = null;
function go(data) {
  res = g.next(data);
  if (!res.done) {
    res.value.then((data) => go(data));  // 这里是await Promise()的过程
  } else {
    return res.value;
  }
}
go() // 由于内部是异步的,这里会返回undefined

这里的go()函数就是驱动整个生成器异步执行的async函数。不过由于内部是异步执行的,所以go()执行完会直接返回undefined,拿不到最后的结果,因而有必要封装一个Promise获取返回值。

const g = gen();
let res = null;
function wrap() {
  return new Promise((resolve) => {  // 这里是async函数返回Promise的过程
    function go(data) {
      res = g.next(data);
      if (res && !res.done) {
        res.value.then((data) => go(data)); // 这里是await Promise()的过程
      } else {
        resolve(res.value); // 通过这一步把return num3的结果送出来
      }
    }
    go();
  });
}

wrap().then((data) => {
  console.log(data);
});

wrap()返回的Promise就是async/await的第一个特性:

async函数返回一个Promise

所有我们就能够理解async/await的两个特性不是偶然的,而是基于生成器实现同步写法实现异步执行目标的必须。

用生成器实现async/await的完整代码

上一部分已经理解了基于生成器实现async/await的基本原理,接下来就完善一下最终的代码:

// 返回一个生成器驱动函数
function generatorToAsync(genFn) {
  // 驱动函数的主体
  return function () {
    const gen = genFn.apply(this, arguments); // gen有可能传参
    // async会返回一个Promise
    return new Promise((resolve, reject) => {
      // 构建递归函数
      function go(key, arg) {
        // 执行生成器
        let res;
        try {
          res = gen[key](arg);
        } catch (error) {
          // 生成器执行出错了
          return reject(error);
        }
        const { value, done } = res;
        // 执行完,退出(边界条件)
        if (done) return resolve(value);
        // 未执行完,继续递归
        // Promise.resolve(value)用于实现await解析后面Promise的值
        return Promise.resolve(value).then(
          (val) => go('next', val),
          // await后面Promise执行出错了
          (err) => go('throw', err)
        );
      }
      go('next');
    });
  };
}

const asyncFn = generatorToAsync(gen);
asyncFn().then((res) => console.log(res));

尾声

技术这东西就是不能糊里糊涂,每次梳理都有新的感悟。