手写一个 async/await

216 阅读4分钟

ES7 规范 async await 原理

一、什么是 async/await?

async/await 是 ES7 引入的新规范,称之为协程。对于携程官方有段描述大致如下:一个线程里可以有多个协程,但是同时只能执行一个协程。

二、async/await 和 generator/promise 是什么关系

async/await 是 generator/promise 实现机制的语法糖。也就是说 async/await 的本质就是 generator/promise 的组合而已,关于 promise 我就不再细说了,不大了解的童鞋可以自行前往官网。

三、手动实现一个 async/await

首先我们来看下,一个标准的 async/await 函数写法:

async function foo() {
  const res = await 'generator';
  console.log(res) // 返回 generator
}

foo()

其中 async 关键字表示的是该函数是个协程函数,并且该函数的返回值是 promise,而 await 关键字等同于 generaor 的 yield

那么 generator 函数长啥样呢?

function* foo() {
  const res = yield 'generator';
  console.log(res); // 返回一个对象 {done: false, value: 'generator'}
}

foo().next(); // generator 函数的返回 yield 值得方法

我们在把这段代码改装一下

function* foo() {
  const res = yield Promise.resolve('generator');
  console.log(res); // 返回一个对象 {done: false, value: Promise}
}

foo().next(); // generator 函数的返回 yield 值得方法

我们的 generator 函数返回的是一个 Promise 函数,而不像 async/await 函数一样直接返回结果字符串,当然我们可以这样:

function* foo() {
  const res = yield Promise.resolve('generator');
  res.then((str) => {
    console.log(str); // 返回 generator
  });
}

foo().next(); // generator 函数的返回 yield 值得方法

虽然也能拿到我们想要的结果,但是确很麻烦并且代码也不美观,如果这时候再多加一个 yield 返回 Promise 结果可想而知。那么 我们怎样才能像 async/await 直接返回想要的结果呢?我们接着来改装一下 foo 函数:

function* foo() {
  const res = yield Promise.resolve('generator');

  console.log(res); // 返回 generator
}

function co(gen) {
  gen = gen();
  result = gen.next();

  return result.value.then((res) => {
    return gen.next(res);
  });
}

co(foo);

经过进一步改装之后,我们也能够像 async/await 在 foo 函数里直接拿到 generator 结果字符串,这就成功模拟实现了 async/await 语法糖。 接下来我们来一步步分析下这段代码:

function co(gen) {
  gen = gen();
  const result = gen.next(); // 返回一个对象 {done: false, value: Promise}

  return result.value.then((res) => {
    // 执行 Promise函数
    return gen.next(res); // 将结果返回给 yield 变量,generator 函数的 next(param) 方法支持传递一个参数,如果传入参数则把参数值返回给 yield 前面声明的变量,如上 foo 中的 const res = yield Promise.resolve('generator'); ,res的就是可以用来接收 next(param) 中的 param 值
  });
}

co 函数允许传入一个参数,很显然这个参数就是我们的 foo 协程函数,所以 可以直接声明一个 result 函数来接收返回值,至此就实现了 async/await 的基本功能,我们测试下:

function co(gen) {
  gen = gen();
  const result = gen.next(); // 返回一个对象 {done: false, value: Promise}

  return result.value.then((res) => {
    // 执行 Promise函数
    return gen.next(res); // 将结果返回给 yield 变量,generator 函数的 next(param) 方法支持传递一个参数,如果传入参数则把参数值返回给 yield 前面声明的变量,如上 foo 中的 const res = yield Promise.resolve('generator'); ,res的就是可以用来接收 next(param) 中的 param 值
  });
}

function* foo() {
  const res = yield Promise.resolve(1);
  console.log(res); // 返回1
}

// 执行co函数

co(foo);

我们可以看到 foo 函数就类似于 async/await 版的

async function foo() {
  const res = await Promise.resolve(1);
  console.log(res); // 返回1
}

我们会发现这边还是会有一个问题,那就是这里的例子只有一个 yield ,实际中我们会有多个 yield ,如下:

function co(gen) {
  gen = gen();
  const result = gen.next(); // 返回一个对象 {done: false, value: Promise}

  return result.value.then((res) => {
    // 执行 Promise函数
    return gen.next(res); // 将结果返回给 yield 变量,generator 函数的 next(param) 方法支持传递一个参数,如果传入参数则把参数值返回给 yield 前面声明的变量,如上 foo 中的 const res = yield Promise.resolve('generator'); ,res的就是可以用来接收 next(param) 中的 param 值
  });
}

function* foo() {
  const res1 = yield Promise.resolve(1);
  console.log(res1);

  const res2 = yield Promise.resolve(2);
  console.log(res2);
}

// 执行co函数

co(foo);

执行完上面代码之后,我们在控制台里发现,只打印出了 1,这是为什么呢?因为我们的 co 函数只执行了一次 result.then ,而我们的测试用例是 2 个 promise,如何解决呢?递归就行。我们改进一下 co 函数

function co(gen) {
  gen = gen();

  // 封装 dep 函数,做递归调用
  function dep(data) {
    const result = gen.next(data); // 返回一个对象 {done: false, value: Promise}

    if (result.done) {
      // 终结条件:没有 yield 退出,避免死循环
      return;
    }

    return result.value.then((res) => {
      // 执行 Promise函数
      return dep(res); // 将结果返回给 yield 变量,generator 函数的 next(param) 方法支持传递一个参数,如果传入参数则把参数值返回给 yield 前面声明的变量,如上 foo 中的 const res = yield Promise.resolve('generator'); ,res的就是可以用来接收 next(param) 中的 param 值
    });
  }

  dep();
}

function* foo() {
  const res1 = yield Promise.resolve(1);
  console.log(res1);

  const res2 = yield Promise.resolve(2);
  console.log(res2);
}

// 执行co函数

co(foo);

执行完改进的 co 函数后,我们发现能够在控制台上看到 1 和 2。至此,一个简化版的 co 函数就实现了,当然完整的 co 函数处理的细节还很多,但是基本的核心代码我们就算实现完成了,有不足之处,欢迎在评论中指出。