async/await的简单实现

286 阅读7分钟

1. 开始

关于async和await的定义,下面分别是MDN和Ai的解释:

MDN

async function 声明创建一个绑定到给定名称的新异步函数。函数体内允许使用 await 关键字,这使得我们可以更简洁地编写基于 promise 的异步代码,并且避免了显式地配置 promise 链的需要。

Ai:

async/await 是 JavaScript 异步编程的终极简化方案,通过隐藏 Promise 的链式调用细节,让开发者专注于业务逻辑的线性表达,其实现原理基于Promise和生成器(generator)。

上面提到了一些关键词,异步编程、链式调用、Promise和生成器,也提到了async/await的实现原理是基于Promise和生成器。

对于我而言前面的几个关键词都很熟悉,针对生成器似乎有点陌生,什么是生成器?async/await和生成器又有什么关系呢?

2. 前置:Generator 函数

2.1. 特征

上面提到的生成器,也就是Generator函数:Generator - JavaScript | MDN, 它是ES6 提供的一种异步编程解决方案。

下面是Generator函数的一些特征:

  • 使用function关键字定义:Generator函数使用function*关键字定义,而不是普通的function关键字。
  • 可以暂停和恢复执行:Generator函数可以在执行过程中通过yield关键字来暂停执行,并在需要时恢复执行。
  • 可以通过调用生成器对象的next()方法来逐步执行:Generator函数返回一个生成器对象,通过调用生成器对象的next()方法,可以逐步执行Generator函数中的代码。每次调用next()方法时,Generator函数会从上次暂停的地方继续执行,直到遇到下一个yield关键字或函数结束。
  • yield关键字可以返回值:yield关键字可以返回一个值,这个值会成为生成器对象的next()方法返回的对象的value属性。

2.2. 函数执行方式

关于Generator 函数,下面给出一个简单demo:

function* gen() {
  yield "hello";
  yield "world";
  yield "!";
}

let g = gen();
console.log("[ g ] >", g);
console.log("g.next()", g.next());
console.log("g.next()", g.next());
console.log("g.next()", g.next());
console.log("g.next()", g.next());
console.log("g.next()", g.next());

// 下面是运行结果
[ g ] > Object [Generator] {}
g.next() { value: 'hello', done: false }
g.next() { value: 'world', done: false }
g.next() { value: '!', done: false }
g.next() { value: undefined, done: true }
g.next() { value: undefined, done: true }

与普通函数不同的是Generator 函数调用后并不会直接执行,而是返回一个迭代器对象(Generator Object,同样这个迭代器对象可以被for of遍历消费是Symbol.iterator方法的最简单实现)

Generator函数的执行是通过调用迭代器对象示例上的方法进行控制,下面是迭代器对象原型上的一些方法:

[[Prototype]]: Generator
  constructor:GeneratorFunction {prototypeGeneratorSymbol(Symbol.toStringTag): 'GeneratorFunction'}
  next: ƒ next()
  return: ƒ return()
  throw: ƒ throw()
  Symbol(Symbol.toStringTag): "Generator"
  [[Prototype]]: Object

通过调用next方法,依次显示 3 个yield表达式的值,第四次调用next方法时Generator 函数已经运行完毕,next方法返回对象的value属性为undefineddone属性为true。以后再调用next方法,返回的都是这个值。

2.3. next方法的参数

yield表达式本身没有返回值,或者说总是返回undefinednext方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。

下面有一段这样的代码:

function* gen() {
  let result1 = yield "hello";
  console.log('[ gen函数内部打印 result1 ] >', result1)
  let result2 = yield "world";
  console.log('[ gen函数内部打印 result2 ] >', result2)
  let result3 = yield "!";
  console.log('[ gen函数内部打印 result3 ] >', result3)
}

let g = gen();
console.log('[ g.next(1) ] >', g.next(1))
console.log('[ g.next(2) ] >', g.next(2))
console.log('[ g.next(3) ] >', g.next(3))
console.log('[ g.next(4) ] >', g.next(4))

// 下面是运行结果
[ g.next(1) ] > { value: 'hello', done: false }
[ gen函数内部打印 result1 ] > 2
[ g.next(2) ] > { value: 'world', done: false }
[ gen函数内部打印 result2 ] > 3
[ g.next(3) ] > { value: '!', done: false }
[ gen函数内部打印 result3 ] > 4
[ g.next(4) ] > { value: undefined, done: true }

代码中一共执行了四次g.next()入参分别是1-4,gen函数内部是这样执行的:

  1. 执行g.next(1):此时控制台打印第一个yield表达式的值,即“hello”,函数内的代码暂停执行
  2. 执行g.next(2):函数内的代码继续执行,在第2行打印了result1,由于本次执行的next方法传入了一个参数“2”,这个“2”会被当作上一个yield表达式的值赋给result1,所以在gen函数内部打印出来的result1的值为“2”,继续执行第3行代码,控制台打印第二个yield表达式的值,即“world”,函数内的代码暂停执行
  3. 执行g.next(3):函数内的代码继续执行,在第4行打印了result2,由于本次执行的next方法传入了一个参数“3”,这个“3”会被当作上一个yield表达式的值赋给result2,所以在gen函数内部打印出来的result2的值为“3”,继续执行第5行代码,控制台打印第三个yield表达式的值,即“!”,函数内的代码暂停执行
  4. 执行g.next(4):gen函数走到了第6行,但是这个next方法传入了参数4,这个参数4被当作上一个yiled表达式的返回值即result3的值,所以在gen函数内部打印result3为4,后续没有yield表达式,控制台打印undefined,函数代码执行结束

3. Generator 函数应用:异步操作的同步化表达

async/await最重要的特点就是能够将异步代码同步化, 上面举例的代码yield表达式后面跟的都是同步代码,那么如果后面跟异步代码会是怎么样的?

3.1. 场景

我们从案例切入,假设现在我们现在需要实现一个方法initData() 方法,在方法内部需要依次调用两个异步接口,其中需要等待第一个接口的数据返回后,再拿第一个接口的数据调用第二个接口。

3.2. 模拟请求

首先我们定义一个模拟请求的request方法,后续的代码使用这个方法模拟异步请求。

const request = (data = {}, duration = 500) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ ...data, _date: Date.now() });
    }, duration);
  });
};

3.3. initData()方法实现

按照上面的经验,我们的第一版代码实现如下:

const initData = function* () {
  console.log("showLoading");
  let response = yield request();
  console.log("[ response ] >", response);
  let response2 = yield request(response._date);
  console.log('[ 最终数据 ] >',response2)
  console.log("hideLoading");
};

let g = initData()

console.log(g.next())

// 下面是运行结果
showLoading
{ value: Promise { <pending> }, done: false }

我们理想的情况应该是走完initData方法的全部代码,但是由于next方法只执行了一次,就导致initData方法中的代码只能运行到第2行,所以这里我们还需要手动调用next方法来控制函数的执行:

let g = initData();
let result = g.next();

// 手动调用next方法
result.value.then((res) => {
  let result = g.next(res)
  result.value.then((res) => {
    g.next(res);
  });
});

// 下面是运行结果
showLoading
[ response ] > { _date: 1742978765222 }
[ 最终数据 ] > { _date: 1742978765724 }
hideLoading

这样我们initData中的代码就能像同步代码一样运行了,如果我们忽略手动调用的代码,同时把*想成async,yield想成await,基本就和async/await的基本用法一样了。

4. async/await的简单实现

实现自动执行器,无非就是实现Generator函数的自动流程管理,使我们不需要手动调用next方法。

4.1 同步的自动执行器

最简单的方法是使用while循环:

const initData = async () => {
  console.log("showLoading");
  let response = await request();
  console.log("[ response ] >", response);
  let response2 = await request(response._date);
  console.log("[ 最终数据 ] >", response2);
  console.log("hideLoading");
};

// 实现自动执行器函数
const spawn = (generatorFn) => {
  let g = generatorFn();
  let result = g.next();

  while (!result.done) {
    result = g.next(result.value);
  }
};

spawn(initData)

// 下面是执行结果
showLoading
[ response ] > Promise { <pending> }
[ 最终数据 ] > Promise { <pending> }
hideLoading

上面代码中,Generator 函数initData会自动执行完所有步骤。

但是,这也仅仅适合同步操作。如果必须保证前一步执行完,才能执行后一步,上面的自动执行就不可行。

4.2 异步的自动执行器

对于异步操作,我们需要借助Promise:

const asyncSpawn = (generatorFn) => {
  return new Promise((resolve) => {
    let g = generatorFn();

    const next = (data) => {
      let result = g.next(data);

      if (result.done) return resolve(result.value);

      if (result.value instanceof Promise) {
        result.value.then((response) => {
          next(response);
        });
      } else {
        next(Promise.resolve(result.value));
      }
    };

    next();
  });
};
  • 第一步,initData开始执行。
  • 第二步,initData执行到异步操作,进入暂停,执行权转移到Promise的回调中。
  • 第三步,(一段时间后)Promise状态变为resolved,执行then方法回调中的g.next()交还执行权。
  • 第四步,initData恢复执行。

上面代码中,只要 Generator 函数还没执行到最后一步,next函数就调用自身,以此实现自动执行。

最后,我们只需要关注initData中的代码实现,将流程控制交个asyncSpawn函数即可。

const initData = function* () {
  console.log("showLoading");
  let response = yield request();
  console.log("[ response ] >", response);
  yield 123
  let response2 = yield request(response._date);
  console.log("[ 最终数据 ] >", response2);
  console.log("hideLoading");
  return "end"
};

asyncSpawn(initData).then((response)=>{
  console.log("[ response ] >", response);
})

// 执行结果
showLoading
[ response ] > { _date: 1743065649287 }
[ 最终数据 ] > { _date: 1743065649790 }
hideLoading
[ response ] > end