likes
comments
collection
share

生成器函数 模拟 async / await 思路

作者站长头像
站长
· 阅读数 12

前言

首先对这个语法好奇是因为我们常用的 async/await 就是基于生成器函数创建的。async/await 就是生成器函数语法糖

是实际上使用到它的场景非常少,我第一次见到,还是在很久以前 unity3D 语法里有见到过。但当时的我还不知道啥是 js.....在大部分 Web 端开发中也非常少见,自己更是忘了记,记了忘...

那么在了解下其基础内容。

生成器函数 Generator

生成器函数执行时能暂停,后面又能从暂停处继续执行。(如果对 React Fiber 有了解的应该会马上想到 Fiber 也是这个作用,实际上之前也有人在 issue 里问过 为什么不使用 生成器函数 来做 fiber,官方给了 2 个原因,这里就不具体说明了,有兴趣的小伙伴可以搜下)

// 定义一个生成器函数
function* generator(i) {
  yield i
  yield i + 10
}

// 调用生成器函数,返回该生成器的迭代器对象
const gen = generator(10)

console.log(gen.next().value) // 10

console.log(gen.next().value) // 20

// gen.next() 会返回 { value , done } , done 表示是否结束

这个是 MDN 上的例子,简要概括了生成器函数的语法,重点在于 *yield。首先第一步我们是用 function * 创建了一个生成器函数,且在后面我们返回了一个名为 gen迭代器对象

当我们首次调用迭代器的 next() 方法时,生成器函数内的语句会执行到第一个 yield 语句的位置,并返回 yileld 后面的值console.log(gen.next().value) // 10 这里就对应了我们第一次调用,且返回值为 10 ,调用 next().value 可以获取到返回值。

next 中传递参数

调用 next() 方法时,是允许给其传入参数的,且这个参数会传给上一条执行的 yield 语句左边的变量。听着可能会觉得有点拗口,用 MDN 里的例子来说明就是:

function* gen() {
  yield 10
  const x = yield 'foo'
  yield x
}

var gen_obj = gen()
console.log(gen_obj.next()) // 执行 yield 10,返回 10
console.log(gen_obj.next()) // 执行 yield 'foo',返回 'foo'

// 这里如果什么都不传递,会打印 undefined ,原因看下面的 【注】
console.log(gen_obj.next(100)) // 将 100 赋给上一条 yield 'foo' 的左值,即执行 x=100,返回 100
console.log(gen_obj.next()) // 执行完毕,value 为 undefined,done 为 true

// 正常情况下,我们 gen_obj.next(100) 时是执行到第三条 yield 语句,即 yield x
// 由于我们传递了参数,所以这时候,会把这个 参数 赋值给上一条 yield 语句的左值。即 const x = 100
// 所以,最后 yield x 会返回 100

// 【注】如果你不传递参数,那么相当于将 undefined 赋值给了 x,const x = undefined
// 最终会打印 undefined 的

yield*

如果 yield 后面跟着的是 *,则表示将执行权移交给另一个生成器函数,当前生成器暂停执行。

同样用 MDN 上的例子来说明,很清晰明了,点个赞~

function* anotherGenerator(i) {
  yield i + 1
  yield i + 2
  yield i + 3
}

function* generator(i) {
  yield i
  yield* anotherGenerator(i) // 移交执行权
  yield i + 10
}

var gen = generator(10)

console.log(gen.next().value) // 10
console.log(gen.next().value) // 11
console.log(gen.next().value) // 12
console.log(gen.next().value) // 13
console.log(gen.next().value) // 20

async/await

介绍完了生成器函数的基础,那么它是如何实现 async/await 的功能的呢?

首先我们需要明确一点: async/await 能实现的原因在于,用户可以主动触发 gen.next()

const p1 = () => {
  return new Promise((resolve) => {
    console.log('1')
    setTimeout(() => {
      resolve(2)
    }, 1000)
  })
}

我们先创建一个 promise 对象,内容很简单就是先打印 1 ,然后再 1秒之后打印 2,来,我们思索下,再结合上面的知识。

思路:

如果把 p1() 的结果作为 yield右值,那么当用户调用 gen.next() 时,就会在 p1() 这里停住。 然后我们的 p1() 因为是一个 promise ,所以它有 .then() 方法,那么只需要在 .then() 里再调用一次 gen.next() 就可以实现 async/await 的功能

简化版代码:
const p1 = () => {
  return new Promise((resolve) => {
    console.log('1')
    setTimeout(() => {
      resolve(2)
    }, 1000)
  })
}

function* autoAwait(p1) {
  const x = yield p1()
  console.log(x)
}

// 生成生成器的迭代器对象
const a1 = autoAwait(p1)
a1.next().value.then((res) => {
  a1.next(res)
})

可能大家伙会好奇,哎呀,长得也不一样, async/await 才没辣么多代码。但其实你会发现,当我们把 autoAwait 转变一下。

async function autoAwait(p1) {
  const x = await p1()
  console.log(x)
}

他就变成了我们的 async/await 了,实际上当你使用 async/await 时,他内部就是做了类似的操作,只不过没有将这些暴露给用户。

当然我的代码也不够完整,没有任何其他的机制,只是介绍了下其思路,感兴趣的话还是建议大家去了解源码。