generator,async,await的原理

1,017 阅读5分钟

Promise系列文章

  1. Promise - 灵魂三问
  2. Promise - 手写源码
  3. Promise - 功能完善

Promise 中有很多问题,内部还是使用回调的方式,如果回调过多,还是会带来回调地狱 如何解决这个问题? 我们希望将异步方法写的更像同步一点 今天我们来探讨下 generator,co, async, await简单原理

generator 使用

我们首先基于固定语法实现一个基本的 generator 函数

function* gen() {
  yield 1
  yield 2
}
const g = gen()
console.log(g.next()) // {value: 1,done: false}
console.log(g.next()) // {value: 2,done: false}
console.log(g.next()) // {value: undefined,done: true}
  • function*: 这种方式会定义一个生成器函数(generation 函数),返回一个 Generator 对象
  • yield: 用来暂停或者恢复一个生成器函数,也就是我们多次调用 next 内容通过这个参数分开来
  • yield* 表达式用于委托给另一个 generator 或可迭代对象。如果有这个标记,另外一个未会一直走
function* tests() {
  yield 1
  yield 2
}

function* test() {
  yield* tests() // 当yieldf* 后面的也是一个生成器函数的时候,方法暂停和回复还会根据嵌套的来
  yield 3
}
const t = test()
console.log(t.next()) // { value: 1, done: false }
console.log(t.next()) // { value: 2, done: false }
console.log(t.next()) // { value: 3, done: false }
console.log(t.next()) // { value: undefined, done: true }

既然已经理解的基本使用规则,我们来一起看看 generator 的一些运行原理

generator 运行原理

其实我们可以将 generator 的运行原理,看成 switch + 指针来实现。 下面我们将第一段代码用 js 换种简单方式实现

  • 首先我们知道我们去调用 generator 函数的时候,会返回一个对象,并且这个对象有一个 next 的方法,再次调用 next 的方法时候,会返回一个{value:xx,done:false}的对象
function gen() {
  return {
    next() {
      return {
        value: '',
        done: false,
      }
    },
  }
}
  • 当然这个 value 和 done 不是一个死的变量,那么我们需要来想办法实现这个参数的变化,从一开始我们了解到了,指针是实现 generator 的方式,那我们先来设定下指针
// 此处通过闭包得方式,来实现对本次gen函数内部变量的一个保存和保护
function gen() {
  const content {
    next: 0, // 设置下一步的指针变量
    done: flase, // 本迭代是否完成
  }
  ...
}
  • 现在我们有指针,那我们继续我们伟大的目标,实现当不停得调用 next 的时候,我们会返回不同的参数。
// 我们用一个函数来达到我们的操作
function _gen(content) {
  // 通过接收到的content中的下一步指针,我们就可以知道我们下一步操作
  switch (content.next) {
    case 0:
      content.next = 1
      return 1
    case 1:
      content.next = 2
      return 2
    case 2:
      content.done = true
      return undefined
  }
}
  • 有了操作步骤分解的方法,我们就可以不全我们的 next 方法返回值了
// ...
next() {
  value: _gen(content),
  done: content.done
}
// ...

到此我们基本完成了简单实现第一段原生 generator 方法实现的效果。 同时我们比对了 babel 转化的核心代码,虽然更加规范,增加了上一步指针,将封装了 done = false 的方法, 但是实际核心还是指针 + switch。

switch ((_context.prev = _context.next)) {
  case 0:
    _context.next = 2
    return 1

  case 2:
    _context.next = 4
    return 2

  case 4:
  case 'end':
    return _context.stop()
}

下面我们完善下我们自己写的方法

function gen() {
  const content = {
    prev: 0,
    next: 0,
    done: false,
    stop: () => {
      this.done = true
    },
  }
  return {
    next() {
      return {
        value: _gen(content),
        done: content.done,
      }
    },
  }
}
function _gen(content) {
  switch ((content.prev = content.next)) {
    case 0:
      content.next = 1
      return 1
    case 1:
      content.next = 2
      return 2
    case 2:
      content.stop()
      return undefined
  }
}

现在我们对 generator 大概有一个了解了,这时候又有问题来了,我们费力巴切的搞了一堆 generator,是不是偏离主题了~~~

当然不是,下来就让我们进入我们玄妙的 genertor 的世界,来探究他给我们代码可以带来什么

generator + Promise

看到标题我们肯定就知道了,我们要通过 generator + Promise 来实现异步了 但是怎么去实现这个功能呢,按照一般思路,我们可能会写出这样的来吧

const fs = require('fs').promises
function* getData() {
  let path1 = yield fs.readFile('./path.txt', 'utf-8')
  let name = yield fs.readFile(path1, 'utf-8')
  return name
}
const _fs = getData()
_fs
  .next()
  .value.then((rs) => {
    console.log(rs)
    _fs
      .next(rs)
      .value.then((rs) => {
        console.log(rs)
      })
      .catch((error) => {
        console.log(error)
      })
  })
  .catch((error) => {
    console.log(error)
  })

但是这个做法好像并没有起到我们今天的目标,反而似的 Promise 变得更加复杂了

co 库

我们继续回想下我们的目标,是不是想要将复杂得 Promise 尽量改成我们想要的如同步一样的写法,我们在此看下上面的 getDate 方法

function* getData() {
  let path1 = yield fs.readFile('./path.txt', 'utf-8')
  let name = yield fs.readFile(path1, 'utf-8')
  return name
}

是不是很接近我们想要的了,那我们下面应该是,想办法让这个方法能够自动执行,而不是要我们自己去写一堆 Promise 处理方法。 这个 co 库已经给我做了解决方案,我们来简单地看看它的实现的方式。

function co(it) {
  // 我们最终返回一个Promise
  return new Promise((resolve, reject) => {
    // 循环回调,一直到generator最后done为false的时候,不能再迭代的时候
    function step(data) {
      // 迭代一次,获取当前步骤内容
      let { value, done } = it.next(data)
      if (!done) {
        // 当前不知道具体步骤返回的值是多少,通过Promise.resolve(),得到then的链式调用内容,
        Promise.resolve(value).then((data) => {
          step(data)
        }, reject)
      } else {
        // 当所有迭代走完,统一返回出去
        resolve(value)
      }
    }
    step()
  })
}

下面我们使用 co()方法来修改读文件例子

function* getData() {
  let path1 = yield fs.readFile('./path.txt', 'utf-8')
  let name = yield fs.readFile(path1, 'utf-8')
  return name
}
co(getData).then((data) => {
  console.log(data) //圩上——TAO
})

噢噢噢噢哦哦哦哦哦哦~~~~

让我们继续看一眼这一段代码,是不是突然觉得很熟悉,是的,你想的没有错,就是 async + await

async + await

可以这么说 async + await === generator + co

async function getData() {
  let path1 = await fs.readFile('./path.txt', 'utf-8')
  let name = await fs.readFile(path1, 'utf-8')
  return name
}
getData().then((data) => {
  console.log(data)
})
// 圩上——TAO