ES6-Generator 函数 和 async 函数

3,946 阅读7分钟

generator 函数是什么?

generator -> 生产者 yield -> 产出

generator 函数是 ES6 提供的一种异步编程解决方案

执行 generator 函数返回的是一个遍历器对象, 也就是说, 我们可以使用 next 方法, 来遍历 generator 函数内部的每一个状态

既然 generator 函数内部具有多个状态, 那么总该有一个标识来决定函数在遍历过程中应该在哪里停下来, 所以我们需要 yield


yield 语句

下面通过一个简单实例, 详细解释 yield 的工作原理

function* foo() {
    yield 'hello'
    
    console.log('come from second yield')
    yield 'world'
    
    return 'ending'
}

const g = foo()

// 执行过程
> g.next()
< { value: 'hello', done: false }

> g.next()
  log: come from second yield
< { value: 'world', done: false }

> g.next()
< { value: 'ending', done: true }

> g.next()
< { value: undefined, done: true }
  • 执行 foo 函数会返回一个遍历器对象, 调用遍历器对象的 next 方法, 使指针移向下一个状态
  • 每次调用 next 方法, 内部指针就从函数头部上一次停下来的地方开始执行, 直到遇到下一条 yield 语句或 return 为止
  • 关于 next 方法的返回对象:
    • value: 指紧跟在 yield 或者 return 后的值, 或者表达式的结果
    • done: 指遍历是否结束, 布尔类型
  • 如果内部状态遇到了 return 语句, 则会直接结束遍历, 也就是说, 之后无论后面还有没有表达式, 或 yield 语句, 再次调用 next 方法, 都只会返回 { value: undefined, done: true }

从 generator 的行为可以看出, 实际上等同于 javascript 提供了一个手动 "惰性求值" 的语法功能


注意事项:

  1. yield 语句不能在普通函数中使用, 否则会报错
  2. yield 语句如果在一个表达式中, 必须放在圆括号里面, console.log('hello' + (yiled 123))

generator 函数传参和 next 方法传参

generator 函数是可以传递参数的, 但 generator 函数有两种传参的方式

generator 函数传参

generator 函数传参跟普通函数传参是一样的, 一个参数能在 generator 函数的任何状态中读取到

function* foo(x) {
    console.log(x)
    yield 'step 1'
    
    console.log(x)
    yield 'step 2'
    
    console.log(x)
    return 'step 3'
}

const g = foo('hello world')

以上代码片段, 无论在函数体的哪一个状态, 都能读取到参数 hello world

next 方法传参

next 方法传递参数, 就跟普通函数传参完全不一样了

yield 语句本身没有返回值, 或者说总是返回 undefined, next 方法可以带一个参数, 该参数会被当做该状态之前所有 yield 语句的返回值

yield 语句没有返回值?

我们先来看看下面的表达式

function* foo() {
  const x = yield 10

  console.log(x)
  return 'ending'
}

const g = foo()

如果我们没有事先知道结果, 肯定会认为这里打印的 `x` 是 10 然而答案却是 undefined, 这也是为什么说 yield 是没有返回值的 ``` javascript > g.next() < { value: 10, done: false }

g.next() log: undefined < { value: 'ending', done: true }

<br>
如果我们希望打印出来的 `x` 的值是 `hello world`, 就必须使用 next 方法传递参数
该参数会被当做上一条 yield 语句的返回值
``` javascript
> g.next()
< { value: 10, done: false }

> g.next('hello world')
  log: hello world
< { value: 'ending', done: true }

练习题

利用 generator 计算

function* foo(x) {
    let y = 2 * (yield (x + 5))
    let z = yield y / 4 + 3
    return (x + y - z)
}

const g = foo(10)
g.next()  // 1
g.next(4) // 2
g.next(8) // 3

运算过程:

1.
x = 10
yield (10 + 5) => 15
> { value: 15, done: false }

2.
y = 2 * 4 => 8
yield (8 / 4 + 3) => 5
> {value: 5, done: false}

3.
x = 10
y = 8 // 保留了上一次 next 方法执行后的值
z = 8
return (10 + 8 - 8) => 10
> { value: 10, done: true }

generator 内部错误捕获

  • generator 函数能够在函数体内部捕获错误
  • 错误一经捕获, generator 函数就会停止遍历, done = true
function* foo() {
  try {    
    yield console.log(variate)
    yield console.log('hello world')
  } catch(err) {
    console.log(err)    
  }
}

const g = foo()

g.next()

> ReferenceError: variate is not defined
    at foo (index.js:3)
    at foo.next (<anonymous>)
    at <anonymous>:1:3
> { value: undefined, done: true }

如果在内部 catch 片段中将错误使用全局方法 throw 抛出, 该错误依然能够被外部 try...catch 所捕获:

function* foo() {
  try {    
    yield console.log(variate)
    yield console.log('hello world')
  } catch(err) {
    throw err 
  }
}

const g = foo()

try {
    g.next()
} catch(err) {
    console.log('外部捕获', err)
}

> 外部捕获 ReferenceError: variate is not defined
    at foo (index.js:3)
    at foo.next (<anonymous>)
    at index.js:13

for...of 循环

因为 generator 函数执行后返回的是一个遍历器对象, 所以我们可以使用 for...of 循环来遍历它

function* foo() {
    yield 1
    yield 2
    yield 3
    return 'ending'
}

const g = foo()

for (let v of g) {
    console.log(v)
}

// < 1
// < 2
// < 3
  • 使用 for...of 循环, 不需要使用 next 语句
  • 一旦 next 方法的返回对象的 done 属性为 true, for...of 循环就会终止
  • for...of 循环终止后不会返回 对象属性 donetrue 的值, 所以上面的例子没有返回 return 里的 ending 值

yield* 语句

该语句用于在 generator 函数内部调用另一个 generator 函数

function* bar() {
    yield 3
    yield 4
}

function* foo() {
    yield 1
    yield 2
    yield* bar()
    yield 5
    return 'ending'
}

for (let v of foo()) {
    console.log(v)
}
// < 1 2 3 4 5

yield* 的真相:

  • 该语句实际上完成了对 遍历器对象 的循环
  • 所以它可以被看做是 for...of 的语法糖
  • 它完全可以被 for...of 替代
yield* bar()

# 完全等价于:
for (let v of bar()) {
    yield v
}
  • 甚至它能遍历数组:
function* gen() {
    yield* [1, 2, 3]
    yield* 'abc'
}

for (let v of gen()) {
    console.log(v)
}
// < 1 2 3 a b c
  • 也就是说, 任何只要具备 Iterator 接口的数据结构, 都能够被 yield* 遍历

yield* 可以保存返回值

yield 不同的是(yield 本身没有返回值, 必须由 next 方法赋予), 如果当被 yield* 代理的 generator 函数有 return 语句时, return 返回的值可以被永久保存

function* foo() {
  yield 2
  return 'hello yield*'
}

function* gen() {
  const a = yield 1

  console.log(a) // -> undefined
  const b = yield* foo()

  console.log(b) // -> hello yield*
  yield 2

  console.log(b) // -> hello yield*
  yield 3
}

const g = gen()

使用 yield* 取出嵌套数组的所有成员

const tree = [1, 2, [3, 4, [5, 6, [7, 8, 9]]], 10, 11]

function* loopTree(tree) {
  if (Array.isArray(tree)) {
    for (let i = 0; i < tree.length; i ++) {
      yield* loopTree(tree[i])
    }
  } else {
    yield tree
  }
}

for (let v of loopTree(tree)) {
  console.log(v)
}
  • 创建 generator 函数 loopTree, 该函数接收一个数组, 或是数字作为参数
  • 如果参数是数组, 则循环该数组, 并使用 yield* 调用自身
  • 如果不是数组, 则返回该值

generator 函数的实际运用

异步操作同步化表达

/**
 * 普通 xhr 请求封装
 * @param  {String} url 请求地址
 * @return {void}   void
 */
function call(url) {
  const xhr = new XMLHttpRequest()
  xhr.onreadystatechange = function () {
    if (xhr.readyState == 4) {
      if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
        const res = JSON.parse(xhr.responseText)

        // 3. 请求成功, 将请求结果赋值给 result 变量, 并进入下一个状态
        g.next(res)
      } else {
        console.log(`error: ${xhr.status}`)
      }
    }
  }
  xhr.open('get', url, true)
  xhr.send(null)
}

function* fetchData() {
  // 2. 发送 xhr 请求
  const result = yield call('https://www.vue-js.com/api/v1/topics') 
  
  // 4. 打印出请求结果
  console.log(result)
}

const g = fetchData()

// 1. 开始遍历 generator 函数
g.next()

部署 Iterator 接口

const obj = {
  name: '木子七',
  age: '25',
  city: '重庆'
}

/**
 * 部署 Iterator 接口
 * @param  {Object} obj 对象
 * @yield  {Array}  将对象属性转换为数组
 */
function* iterEntires(obj) {
  let keys = Object.keys(obj)
  for (let i = 0; i < keys.length; i ++) {
    let key = keys[i]
    yield [key, obj[key]]
  }
}

for (let [key, value] of iterEntires(obj)) {
  console.log(key, value)
}

< name 木子七
< age 25
< city 重庆

与 Promise 结合

generator 与 Promise 结合后, 实际上就是 async/await 的封装实现, 下面小结会详细描述 async/await

原理

// 1. 定义 generator 函数
// - 其返回的原生 fetch 方法是个 Promise 对象
function* fetchTopics() {
  yield fetch('https://www.vue-js.com/api/v1/topics')
}

const g = fetchTopics()

// 2. 调用 next 方法
const result = g.next()

// 3. g.next() 返回的 { value: ..., done } 中的 value 就是 Promise 对象
result.value
  .then(res => res.json())
  .then(res => console.log(res))

封装的方法实现

/**
 * 封装用来执行 generator 函数的方法
 * @param {Func} generator generator 函数
 */
function fork(generator) {
  // 1. 传入 generator 函数, 并执行返回一个遍历器对象
  const it = generator()

  /**
   * 3. 遍历 generator 函数中的所有 Promise 状态
   *    go 函数会不停的使用 next 方法调用自身, 直到所有状态遍历完成
   * @param {Object} result 执行 next 方法后返回的数据
   */
  function go(result) {
    if (result.done) return result.value

    return result.value.then(
      value => go(it.next(value)),
      error => go(it.throw(error))
    )
  }

  // 2. 初次执行 next 语句, 进入 go 函数逻辑
  go(it.next())
}

/**
 * 普通的 Promise 请求方法
 * @param {String} url  请求路径
 */
function call(url) {
  return new Promise(resolve => {
    fetch(url)
      .then(res => res.json())
      .then(res => resolve(res))
  })
}

/**
 * 业务逻辑 generator 函数
 * - 先请求 topics 获取所有主题列表
 * - 再通过 topics 返回的 id, 请求第一个主题的详情
 */
const fetchTopics = function* () {
  try {
    const topic = yield call('https://www.vue-js.com/api/v1/topics')

    const id = topic.data[0].id
    const detail = yield call(`https://www.vue-js.com/api/v1/topic/${id}`)

    console.log(topic, detail)
  } catch(error) {
    console.log(error)
  }
}

fork(fetchTopics)

async 函数

async 函数属于 ES7 的语法, 需要 Babelregenerator 转码后才能使用

async 函数就是 Generator 函数的语法糖, 其特点有:

  • Generator 函数的执行必须依靠执行器, 而 async 函数自带执行器, 也就是说, async 函数的执行与普通函数一样, 只要一行
  • 不需要调用 next 方法, 自动执行
  • async 表示函数里有异步操作, await 表示紧跟在后面的表达式需要等待结果
  • 要达到异步操作同步执行的效果, await 命令后面必须是 Promise 对象, 如果是其他原始类型的值, 其等同于同步操作
function timeout() {
  return new Promise(resolve => {
    setTimeout(resolve, 2000)
  })
}

async function go() {
  await timeout().then(() => console.log(1))
  console.log(2)
}

go()

// 执行输出, 先输出1 后输出2
// -> 1
// -> 2
  • async 函数的返回值是 Promise 对象, 而 Generator 函数返回的是 Iterator 对象, 所以我们可以用 then 方法指定下一步操作
function timeout() {
  return new Promise(resolve => {
    setTimeout(resolve, 2000)
  })
}

async function go() {
  await timeout().then(() => console.log(1))
  console.log(2)
}

go().then(() => console.log(3))
// -> 1
// -> 2
// -> 3
  • 可以说, async 函数完全可以看做由多个异步操作包装成的一个 Promise 对象, 而 await 命令就是内部 then 命令的语法糖

下面我们使用 async 函数来实现之前的异步请求例子

const call = url => (
  new Promise(resolve => {
    fetch(url)
      .then(res => res.json())
      .then(res => resolve(res))
  })
)

const fetchTopics = async function() {
  const topic = await call('https://www.vue-js.com/api/v1/topics')

  const id = topic.data[0].id
  const detail = await call(`https://www.vue-js.com/api/v1/topic/${id}`)

  console.log(topic, detail)
}

fetchTopics().then(() => console.log('request success!'))

总结: async 函数的实现比 Generator 函数简洁了许多, 几乎没有语义不相关的代码, 它将 Generator 写法中的自动执行器改在了语言层面提供, 不暴露给用户, 因此减少了许多代码量