阅读 5252

实现一个 async/await (typescript 版)

写在前面

距离我上一篇文章已经过去了两个月了,这两个月因为一直在忙实习面试的事情,所以文章产出这边就稍微耽搁了一下(其实就是懒),最后也成功拿到了某大厂的 offer,现在已经入职实习了,也算是 2021 年的第一个喜讯吧(笑)。好了,不多说,接上篇 实现一个符合 Promise/A+规范的 Promise(typescript 版)。这次我们来实现一个 typescript 版本的 async/await。

关于 async/await 的原理的文章,网上也有很多了,但是本文既然是使用 typescript 来写,我们的 async/await 也是要能够通过用户传入的函数自动推断出结果,所以如何对其编写 typescript 定义也是本文的一个重要板块。

何为 async/await

用红宝书上的话说,"async/await" 其实就是异步函数,是 Promise 在函数中的应用,ES2017 (ES8)中新增的规范。这个新增的特性能够让以同步方式写的代码能够异步执行

基本用法

具体的使用方法在这里就不再赘述了,下面是一般的使用方法:

// success
async function fn1() {
  /*
    如果 await 一个 Promise,成功时可以直接将 Promise 的 fulfilled 的值取出
    如果是一个非 Promise 的值,await 可以看作不存在,不会有任何实际的作用。
  */
  const res = await new Promise<string>((resolve, reject) => {
    setTimeout(() => {
      resolve('fulfilled')
    }, 2000)
  })
  console.log(res) // fulfilled
  return res
}
// error
async function fn2() {
  try {
    /* 
      如果 await 的 Promise 的状态为 rejected,那么就会抛出一个同步的错误,
      该错误即使不 try/catch 也不会阻止程序正常运行,因为 async 本质也是
      在函数外部套了一层 Promise,会直接触发 UnhandledPromiseRejectionWarning
    */
    const res = await new Promise<string>((resolve, reject) => {
      setTimeout(() => {
        reject('rejected')
      }, 2000)
    })
    return res
  } catch (error) {
    console.log(error) // rejected
    return Promise.reject(error)
  }
}
复制代码

对于 async/await 函数的返回值,这里要细说一下,正如之前所讲,async 会为整个函数外部包一层 Promise,所以在函数内部 return 值的时候相当于在 Promise 的then回调中 return 值,返回值都会是被 Promise 包裹后的。

本质

OK.我想各位对于 async/await 的基本用法应该都很熟悉,也应该大致听过它的本质,该特性其实归根结底就是一个语法糖,是由 ES6 中提出的 Generator 函数和 Promise 共同完成的,Promise 相信大家都很熟悉,而 Generator 函数又会返回一个 Iterator 接口,那么 Generator 函数和 Iterator 接口又是什么东西呢,下面再来进行探讨。

Iterator

迭代器(Iterator)是一种接口,或者说是一种机制。它能为各种不同的数据结构提供统一的访问机制,任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。主要的作用是为了为各种数据结构提供一个统一的简便的访问接口,使数据结构的成员能够按某种次序排列。

Iterator本质上是一个指针对象,实现过程如下:

  1. 创建一个指针对象,指向当前数据结构的起始位置。
  2. 第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
  3. 第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。
  4. 不断调用指针对象的next方法,直到它指向数据结构的结束位置。

原生数据结构中的 Iterator

在 JS 中,有一些内置的数据结构天生就具有默认的 Iterator 接口,包括:

  • Array
  • Map
  • Set
  • String
  • 函数的 arguments 对象
  • NodeList 对象

要获取到这些数据结构中的 Iterator 接口,需用调用Symbol.iterator方法:

// 数组的Symbol.iterator方法
const arr = ['a', 'b', 'c'];
const iter = arr[Symbol.iterator]();
// 通过next()方法实现每一次的迭代器的遍历
iter.next() // { value: 'a', done: false }
iter.next() // { value: 'b', done: false }
iter.next() // { value: 'c', done: false }
iter.next() // { value: undefined, done: true }
复制代码

其中value是每次遍历到的值,done代表是否将该数组遍历完全。

自定义的 Iterator

正如前面所说,Iterator 只是为了给我们提供一个统一的访问接口,所以任何一个普通的对象都是可以实现 Iterator 的,只需要定义Symbol.iterator方法就可以了。

const iterObj = {
  value: 0,
  [Symbol.iterator]() {
    const self = this
    return {
      next() {
        const value = self.value++
        const done = value > 2
        return {
          value: done ? undefined : value,
          done
        }
      }
    }
  }
}

const iter = iterObj[Symbol.iterator]()

iter.next() // { value: 0, done: false }
iter.next() // { value: 1, done: false }
iter.next() // { value: 2, done: false }
iter.next() // { value: undefined, done: true }
iter.next() // { value: undefined, done: true }
复制代码

我们实现的 Iterator 接口需要有next方法(这个方法会在类型for...of这样的语句中会被调用),同时在next方法中要对完成状态和未完成的状态做判断,当全部迭代完毕后应该把done设置为true,同时返回的value应该为undefined

在本文中,我们对于 Iterator 的理解只需要懂得其基本概念就可以了,如果想深入了解可以去 MDN 上面查看。

其他方法

迭代器除了有必须要实现的next方法,还可以有两个可选的方法,分别是returnthrow方法。

return 方法

return方法用于指定在迭代器提前关闭时执行的逻辑。当我们不想让遍历到的可迭代对象被耗尽时,就可以将迭代器"关闭"。可能的情况有:

  • for...of循环通过breakcontinuereturnthrow提前退出。
  • 解构操作并未消费所有值。

比如在如果在 Generator 函数返回的迭代器对象中调用return方法就可提前将其关闭:

function* gen() {
  yield 1;
  yield 2;
  yield 3;
}

const g = gen();

console.log(g.next());// { value: 1, done: false }
// 同时该方法还可传递参数,而向一般的迭代器的 return 方法传入参数是没有用的
console.log(g.return('foo'))// { value: "foo", done: true }
console.log(g.next());// { value: undefined, done: true }
复制代码

可以发现,调用return方法后迭代器就已经是完成状态了。

值得注意的是,因为该方法是可选的,所以并非所有迭代器都是可关闭的,比如数组的迭代器就不可关闭:

const a = [1, 2, 3, 4, 5]
const iter = a[Symbol.iterator]()
for (const i of iter) {
	console.log(i)
    if(i > 2) {
    	break
    }
}
// 1
// 2
// 3

for (const i of iter) {
	console.log(i)
}
// 4
// 5
复制代码

当然,这个方法在本文中其实并没有用到,这里就当做额外扩展了吧。

throw 方法

throw方法主要是配合 Generator 函数使用,一般的迭代器对象用不到这个方法,我们在后面 Generator 中再细讲。

Generator

Generator 是 ES6 推出的一种新的数据类型,它本质上是 JS 协程的一种实现,至于协程是什么这里就不再多探究了,继续往下就是关于 JS 引擎的相关知识了,有兴趣的小伙伴可以自行查阅资料。

与普通函数的区别

  • 在进行函数声明的时,function关键字与函数名之间有一个星号。同时不能使用箭头函数进行声明,否则会报错。
  • Generator 函数的返回值与普通函数不同,而是会返回一个迭代器对象,该对象可以依次对 Generator 函数内部的每一个状态进行迭代。
  • 函数体内部使用yield表达式,定义不同的内部状态。
  • Generator 函数不能使用new关 键字,否则会报错。

如何使用

因为本文主要探讨的问题是如何实现 async/awiat,所以关于 Generator 函数的用法只介绍其基本用法和与 async/await 实现相关的部分。

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

const hw = helloWorldGenerator();
console.log(hw);

console.log(hw.next());// {value: "hello", done: false}
console.log(hw.next());// {value: "world", done: false}
console.log(hw.next());// {value: "ending", done: true}
console.log(hw.next());// {value: undefined, done: true}
复制代码

上面代码的解释:

  • Generator 函数调用后该函数并不会运行,也不会返回函数的运行结果,而是返回的迭代器(Iterator) 对象,内部的yield表达式为一个个状态,表达式后面的值会作为该状态的返回值,所以该函数一共有三个状态:helloworldretrun语句结束执行状态ending
  • 如果想要运行到 Generator 函数内部的每一个yield阶段,就必须要调用迭代器的next()方法,使其函数内部的状态指针移向下一个状态,每次调用next()方法时,内部指针就会从函数头部或上一层停下来的地方开始执行,直到运到下一个yiedl表达式或遇到return语句(遇到return语句函数会直接停止),在停止后依然能调用next()方法,但是此时返回的valueundefined

yield

yield 表达式并没有什么特别的地方,它仅仅就只代表一个暂停的标志而已,yield 表达式后面的值相当于一个阶段的值,该值会作为调用相应next()后对象的value属性的值。

迭代器的 next() 方法参数

yield本身没有返回值,或者说总是返回undefined。Generator 函数返回的迭代器的next方法可以带一个参数,该参数就会被当作上一个yield语句的返回值。

function* f() {
  const a = yield 1
  console.log(a) // 'a'
  const b = yield 2
  console.log(b) // 'b'
}

const g = f()

g.next() // { value: 1, done: false }
g.next('a') // { value: 1, done: false }
g.next('b') // { value: undefined, done: true }
复制代码

在传入参数的时候我们应该从第二个next方法开始进行传入,因为第一个next方法是从函数内部起始开始运行的,此时最前方并没有yield表达式,所以第一个next方法中的参数并没有任何作用,如果要在开头就要传入参数,应该在生成迭代器对象时传入 Generator 函数的参数。

function* f(name:string) {
  const a = yield name
  console.log(a) // 'a'
  const b = yield 2
  console.log(b) // 'b'
}

const g = f('Coloring')
g.next() // { value: 'Coloring', done: false }
复制代码

由于 Generator 函数从暂停状态到恢复运行,它的上下文状态(context)是不变的。通过next方法的参数,就有办法在 Generator 函数开始运行之后,继续向函数体内部注入值。也就是说,可以在 Generator 函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为。

迭代器的 throw() 方法

在前面我们说到过,Generator 函数产生的迭代器还有第三个方法throw,这个方法同return方法一样都可以强制让生成器处于关闭状态。

function* generatorFn() {
  for (const x of [1, 2, 3]) {
    yield x
  }
}

const g = generatorFn()

console.log(g) // generatorFn {<suspended>}

try {
  // 未处理会抛出同步错误
  g.throw('foo')
} catch (error) {
  console.log(error) // foo
}

console.log(g) // generatorFn {<closed>}
复制代码

不过如果我们在 Generator 函数内部处理了这个错误,那么生成器就不会关闭,而且还可以恢复执行。错误会跳过对应的yield,如下:

function* generatorFn() {
  for (const x of [1, 2, 3]) {
    try {
      yield x
    } catch (error) {
      console.log(error) // foo
    }
  }
}

const g = generatorFn()

console.log(g.next()) // { value: 1, done: false }
g.throw('foo')
console.log(g.next()) // { value: 3, done: false }
复制代码

相信各位大体可以猜到,我们可以利用这个方法模拟出 async/await 的同步抛出异常,后续我们可以通过拿到rejected状态 Promise 的reason手动将其抛出。

实现 async/await

前面讲了那么多,其实都是为实现 async/await 做准备,由 Generator 函数我们可以知道使用 yield就可以作为函数暂停的标识,但是每次继续运行都需要手动调用迭代器的next方法,而 async/await 实质上就是要简化这种手动调用的方式,让 Generator 函数能够自动进行迭代。

在前面使用 async/await 的时候我们发现,await后面紧跟的值或fulfilled状态的 Promise 的值会直接作为该表达式的返回值。那么只要我们能够将yield后面紧跟的值也作为yield表达式的返回值,并且把 Generator 函数的返回值也用 Promise 进行一层包装,那么是否就能够成功实现 async/await 了呢。

实际上,早已有人根据这个原理实现了相应的库,如:co,那么我们也可以依次为依据对其做一个简单的实现。

实现包装函数

包装函数会运行传入的回调 Generator 函数,拿到其生成的迭代器对象后,在内部调用该迭代器,并返回一个与该迭代器的每个步骤都绑定好的 Promise。

类型探究

我们先来看看使用 async/await 函数时的类型定义是什么样子的:

async function fun(a: string) {
  return a
}
复制代码

当返回 Promise 时:

async function fun(a: string) {
  return Promise.resolve(a)
}
复制代码

可以看到,typescript 自动帮我们为函数推断好了类型定义,同时也会对返回的 Promise 进行自动解包。

既然如此,我们可以先声明好对应的工具类型:

// PromiseLike 是在 ES6 中进行全局定义的定义文件,可以不用引入,这里只是说明其定义,总的来说就是一类 Promise 的实例
interface PromiseLike<T> {
    /**
     * Attaches callbacks for the resolution and/or rejection of the Promise.
     * @param onfulfilled The callback to execute when the Promise is resolved.
     * @param onrejected The callback to execute when the Promise is rejected.
     * @returns A Promise for the completion of which ever callback is executed.
     */
    then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null): PromiseLike<TResult1 | TResult2>;
}

type ResolveValue<T> = T extends PromiseLike<infer V> ? V : T
复制代码

上面的工具类型可以帮助我们将 Promise 成功状态的类型解析出来。

我们再来看看一个正常具有返回值 Generator 函数的类型定义:

function* fun(a: string) {
  const b = yield 'b'
  const c = yield Promise.resolve('c')
  const d = yield Promise.resolve('d')
  return a
}
复制代码

当返回 Promise 时:

function* fun(a: string) {
  const b = yield 'b'
  const c = yield Promise.resolve('c')
  const d = yield Promise.resolve('d')
  return Promise.resolve(a)
}
复制代码

可以看出,一个 Generator 函数的返回类型是 Generator 类型,该类型可以接收三个泛型参数,在途中我们可以隐约猜到:

  • 第一个参数是每一次使用yield表达式后面返回状态的值的类型,这个参数会被自动根据返回状态的值进行类型推断并将所有类型进行合并。

  • 第二个参数是 Generator 函数的返回值,但是我们可以看到,当我们返回一个普通值和fulfilled状态的 Promise 时该值的类型是不同的,而 async/await 中这两个类型确是相同的,并且能够将 Promise 解包,相信各位能够猜出来,我们需要使用之前定义的工具类型ResolveValue对其进行类型校正。

  • 第三个参数我们在图中的类型定义中并不能很清晰的看出来,那我们就去源码定义中看一看:

    interface Generator<T = unknown, TReturn = any, TNext = unknown> extends Iterator<T, TReturn, TNext> {
        // NOTE: 'next' is defined using a tuple to ensure we report the correct assignability errors in all places.
        next(...args: [] | [TNext]): IteratorResult<T, TReturn>;
        return(value: TReturn): IteratorResult<T, TReturn>;
        throw(e: any): IteratorResult<T, TReturn>;
        [Symbol.iterator](): Generator<T, TReturn, TNext>;
    }
    复制代码

    可以看到第个三参数被定义为了TNext,并且在类型定义中有两个地方用到了,一个是next方法的参数中,另一个则是用在了继承接口Iterator的第三个参数中,而Iterator接口其实是这样的:

    interface Iterator<T, TReturn = any, TNext = undefined> {
        // NOTE: 'next' is defined using a tuple to ensure we report the correct assignability errors in all places.
        next(...args: [] | [TNext]): IteratorResult<T, TReturn>;
        return?(value?: TReturn): IteratorResult<T, TReturn>;
        throw?(e?: any): IteratorResult<T, TReturn>;
    }
    复制代码

    所以,我们可以看出第三个参数其实就是next方法的参数类型,然后由之前我们对 Generator 函数的学习也能够知道,next方法中传入的值是会作为 Generator 函数内部的返回值的。

    由此可以得Generator接口的第三个参数是所有yield返回值类型的合并。 不过很遗憾的是,我们目前是无法使用这个参数对我们产生提示的,因为yield表达式是在 Generator 函数内部使用,我们无法在外部对其类型进行修改,除非我们手动进行标注:

    function* fun(
    a: string
    ): Generator<Promise<string> | 'b', Promise<string>, string> {
      const b = yield 'b'
      const c = yield Promise.resolve('c')
      const d = yield Promise.resolve('d')
      return Promise.resolve(a)
    }
    复制代码

    可以看到,还是可以通过该泛型的类型为我们提供提示的,但是这样写的缺点也应该看到了,特别的麻烦,同时需要自己对三个泛型参数进行定义,并且由于是合并的类型,所以对于类型的提示其实非常的糟糕,最好的做法应该是在函数内部进行变量声明的时候直接定义类型:

    function* fun(
    a: string
    ) {
     const b: string = yield 'b'
     const c: string = yield Promise.resolve('c')
     const d: string = yield Promise.resolve('d')
     return Promise.resolve(a)
    }
    复制代码

    注:

    • 其实第三个泛型参数本身的自动推断就是由内部需要接收yield返回值的变量类型来反向推断出的,但是由于我们是模拟 async/await,所以是从正向定义定义进行思考的。 可以看到第三个泛型参数的值被自动补充上去了。
    • async/await 是能够不用任何的类型定义就能自动推断出await表达式后面的类型的,同时它会将fulfilled状态的 Promise 自动解包出来,所以类型也是有细微不同的,这个主要是因为yieldawait最初的设计用法就不一致,我们只是使用yield这一暂停标志来模拟出await的功能而已,所以并不能做到完美重现。

    我踩过的坑

    在最开始的时候我也想过要做一个能够自动解析传入函数类型的包装函数,因为我们可以看出由于是自动迭代,所以第一个参数的类型应该是和第三个参数类型一致,我们只需要将这两个设为一样就可以了,但是这是很不合理的,因为我需要做的是通过传入的函数本身自动推断 Generator 接口的泛型参数值,然后再直接改变它的泛型参数值,而目前 typescript 是不能支持我这样做的。

    后续我又想通过类型断言的方式实现,这样只需要写一下对应的类型定义就可以了,如同上面最开始那样需要自己将参数全部补齐的方法有点让人难以接受,所以我又定义了一个工具类型:

    type AsyncFunction<T> = T extends (
    ...args: infer A
    ) => Generator<infer Y, infer R, unknown>
      ? (...args: A) => Generator<Y, ResolveValue<R>, ResolveValue<Y>>
      : never
    复制代码

    最初的想法是通过传入函数本身的类型来再次进行类型推断,但是写完的时候发现又忽略了函数本身的自引用问题,要做到类型的自动推断就需要在外部写一个一模一样的函数funClone,然后使用AsyncFunction<typeof funClone>这样的方式来进行强制类型推断,这样明显工作量会更加庞大,而且这其中还会伴随着类型冲突的问题,所以果断放弃(哭)。

好了,研究完毕 Generator 的类型定义后,我们就需要依靠它所提供的泛型参数为我们的包装函数设计类型定义了。

类型定义

首先,我们要明确我们做的是一个包装函数,也就是一个高阶函数,所以我们应该返回一个函数,这个函数的类型定义应该和我们使用 async/await 定义的函数类型一致。

function _asyncToGenerator<R, T = unknown, A extends Array<any> = Array<any>>(
  fn: (...args: A) => Generator<T, R, any>
): (...args: A) => Promise<ResolveValue<R>> {
  // ...
}
复制代码

上面就是我们的包装函数的类型定义了,我们一步步来细看:

  • 使用泛型参数: 如果我们要写一个能够自动根据传入值类型推断出后续类型的接口时,泛型参数是必须的一环,通过在对应位置填写对应的泛型参数,我们就能够让 typescript 自动为我们的泛型参数赋值,从而在其他地方使用时也能自动进行推断了。

  • R泛型(也就是 Generator 函数返回值)放在第一个: 由于TA都只是用做类型推断的工具人,而R可以用做手动传入来控制 Generator 函数的返回值,对比 async/await,当对于返回值类型不明确时,我们也可以手动为 async/await 函数标注返回值控制其返回类型。

  • 传入参数为一个 Generator 函数: 因为我们是需要通过该传入的函数进行类型推断的,所以我们需要在这里使用泛型参数获取类型。

  • 返回值为 一个返回值为 Promise 的函数 正如我们最开始的需求,对传入 Generator 函数的类型获取,就是为了对包装后的函数做正确的类型推断。

    该函数的形参类型是与 Generator 函数一致的,同时返回值类型是对 Generator 函数的返回值类型做了 Promise 的解包处理后的结果,并在外面包裹了一层 Promise,就如同 async/await 函数。

逻辑编写

又说了半天,现在终于进入到了真正的逻辑代码编写中了。正如一开始说的,如何编写 typescript 定义也是本文的一个重要板块, 可能这就是 typescript 爱好者的强迫症吧(笑)。在我的理解中,先从类型定义出发对需求做一个整体感知是能够让后续业务开发更加快速的,下面就从类型定义的角度来进行编写:

function _asyncToGenerator<R, T = unknown, A extends Array<any> = Array<any>>(
  fn: (...args: A) => Generator<T, R, ResolveValue<T> | any>
): (...args: A) => Promise<ResolveValue<R>> {
  // 需求一:我们要返回一个函数
  return function (this: void, ...args) {
    // 内部的 this 需要显示定义类型,因为该函数不是构造函数,所以类型为 void 就行了
    const self = this
    // 需求二:返回函数的函数值需要被 Promise 包裹
    return new Promise(function (resolve, reject) {
      // 需求三:返回的 Promise 最终的返回值就是 Generator 的返回值,那么这个返回值如何得到呢,我们需要获取到迭代器一直执行下一步,直到遍历到最后 done 的状态第一次为 true
      // 获取迭代器实例,将外层的函数作为 this 传入
      const gen = fn.apply(self, args)
      // 因为我们要完成自动迭代,所以需要对 next 和 throw 方法做一层包装
      // 执行下一步
      function _next(...nextArgs: [] | [T]) {
       // 需求四:使用 yield 模拟 await,同时需要自动迭代,next 方法的返回值的 value 属性是下一次 next 方法的参数
       // 我们需要在这里面封装一个自动迭代的函数 asyncGeneratorStep
       // asyncGeneratorStep()
      }
      // 需求五:当遇到 rejected 的 Promise 时在 Generator 内部抛出同步异常
      function _throw(err: any) {
       // 因为如果捕获到了异常,那么还需要继续往下迭代,所以这里也需要使用自动迭代的函数 asyncGeneratorStep
       // asyncGeneratorStep()
      }
      // 需求六:自动运行迭代器,所以我们需要在函数内部启动迭代器
      _next()
    })
  }
}
复制代码

通过对于需求分析和类型定义我们编写出了最外层的包装函数应该大体要实现的功能,接下来就是对内部自动迭代函数asyncGeneratorStep代码的编写。

实现迭代函数

类型定义

因为迭代函数只供内部,用户在外层是无法感知到的,所以这里就不探究如何编写类型定义了,我们只需要保证我们定义的类型能够辅助我们正确编写函数的逻辑就可以了。

function asyncGeneratorStep<
  R,
  TNext = unknown,
  T extends Generator = Generator
>(
  // 生成器(迭代器)实例
  gen: T,
  // 外层包装 Promise 的 resolve 函数,迭代完毕后最后一次的值就是函数的 return 值
  resolve: (value: R) => void,
  // 外层包装 Promise 的 reject 函数,使用迭代器的 throw 方法同步抛出异常后如果没有捕获我们需要手动 reject 改变 Promise 状态
  reject: (reason?: any) => void,
  // 我们上面自己内部封装的 next 和 throw 函数
  _next: (...args: [] | [TNext]) => void,
  _throw: (err: any) => void,
  // 是继续迭代还是抛出错误
  key: 'next' | 'throw',
  // 只有一个参数,同时需要满足 next 和 throw,所以直接 any 就好了
  arg?: any
): void { // 不需要返回值,因为使用的回调函数
// ...
}
复制代码

逻辑编写

在写代码之前,我们先来看一看具体的运行逻辑: 按照上图与类型定义,我们能很快写出逻辑代码:

function asyncGeneratorStep<
  R,
  TNext = unknown,
  T extends Generator = Generator
>(
  gen: T,
  resolve: (value: R) => void,
  reject: (reason?: any) => void,
  _next: (...args: [] | [TNext]) => void,
  _throw: (err: any) => void,
  key: 'next' | 'throw',
  arg?: any
): void {
  // 需要加 try...catch 将 Generator 函数内部未捕获的异常捕获
  try {
    // yield 表达式后面跟的值,不管 key 是 next 还是 throw 都会返回一样的结构,为了能继续往下面迭代
    const { value, done } = gen[key](arg)
    if (done) {
      // 迭代器完成,直接 resolve 返回值
      resolve(value)
    } else {
      // 将所有值 Promise 化,如果是传入的值是一个 rejected 的 Promise,直接 throw 成同步错误,否则继续往下迭代
      Promise.resolve(value).then(_next, _throw)
    }
  } catch (error) {
  	// 如果 Generator 函数内部未捕获的异常直接 reject
    reject(error)
  }
}
复制代码

可以看到,主要的迭代函数代码并不复杂,只需要理清楚每个阶段应该如何处理状态就可以了。

全部代码

手写 async/await 的所有代码已经解释完了,下面是全部代码:

type ResolveValue<T> = T extends PromiseLike<infer V> ? V : T

function _asyncToGenerator<R, T = unknown, A extends Array<any> = Array<any>>(
  fn: (...args: A) => Generator<T, R, any>
): (...args: A) => Promise<ResolveValue<R>> {
  return function (this: void, ...args) {
    const self = this
    return new Promise(function (resolve, reject) {
      // 获取实例 
      const gen = fn.apply(self, args)
      // 执行下一步
      function _next(...nextArgs: [] | [T]) {
        asyncGeneratorStep(
          gen,
          resolve,
          reject,
          _next,
          _throw,
          'next',
          ...nextArgs
        )
      }
      // 抛出异常
      function _throw(err: any) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'throw', err)
      }
      // 启动迭代器
      _next()
    })
  }
}

function asyncGeneratorStep<
  R,
  TNext = unknown,
  T extends Generator = Generator
>(
  gen: T,
  resolve: (value: R) => void,
  reject: (reason?: any) => void,
  _next: (...args: [] | [TNext]) => void,
  _throw: (err: any) => void,
  key: 'next' | 'throw',
  arg?: any
): void {
  try {
    const { value, done } = gen[key](arg)
    if (done) {
      resolve(value)
    } else {
      Promise.resolve(value).then(_next, _throw)
    }
  } catch (error) {
    reject(error)
  }
}
复制代码

测试一下:

const asyncFunc = _asyncToGenerator(function* (param: string) {
  try {
    yield new Promise<string>((resolve, reject) => {
      setTimeout(() => {
        reject(param)
      }, 1000)
    })
  } catch (error) {
    console.log(error)
  }

  const a: string = yield 'a'
  const d: string = yield 'd'
  const b: string = yield Promise.resolve('b')
  const c: string = yield Promise.resolve('c')
  return [a, b, c, d]
})

asyncFunc('error').then((res) => {
  console.log(res)
})

// error
// ['a', 'b', 'c', 'd']
复制代码

同时,类型也能成功被推断出来,nice。

总结

本文使用 typescript,根据类型定义从零开始实现了一个 async/await,其中重点对 typescript 的类型定义和 Generator 函数的自动迭代逻辑进行了深入。作者技术有限,如果有什么错误或遗漏的地方还请在评论区中指出,顺便求个👍。

参考资料

各种源码实现,你想要的这里都有 - async/await 实现

MDN - Generator

JavaScript 高级程序设计(第4版)

文章分类
前端
文章标签