Generator 和异步迭代

Generator 函数

普通函数只可以返回一个值,或者不返回任何值(实际返回 undefined)

Generator 函数可以返回('yield')多个值, 它们可以与 iterator 配合使用

要创建 Generator 函数,需要用一个特殊的语法function *

javascript
1function* generateSequence() {
2  yield 1
3  yield 2
4  return 3
5}

当调用generator函数时,并不会运行其代码,而是返回一个[object Generator]的特殊对象,我们称其为 “generator object”

javascript
1function* generateSequence() {
2  yield 1
3  yield 2
4  return 3
5}
6const a = generateSequence()
7console.log(a) //[object Generator]

到目前为止,上面的代码还没有运行,我们拿到的是generator对象。

当前代码的位置

generator 对象的主要方法是 next(),当调用该方法时,它就会开始运行,执行到最近的 yield语句,然后函数暂停,将yield的值返回出去。

next()的结果会返回一个对象,该对象有两个属性

  • value:yield 后的值
  • done:如果 generator 执行完成,则返回 true,如果没有则返回false
javascript
1function* generateSequence() {
2  yield 1
3  yield 2
4  return 3
5}
6const a = generateSequence()
7console.log(a)
8console.log(a.next()) // {value:1,done:false}

截止目前,我们获取到了第一个值,现在代码处于这里

image-20211018145002687

当我们再次调用 next方法,代码就会从当前的位置继续往下 yield,直到 done为 true为止

javascript
1console.log(a.next()) // {value:2,done:false}
2console.log(a.next()) // {value:3,done:true}

现在代码已经执行完成了,done的状态为true,如果继续往下执行,会返回 value为undefined的对象

javascript
1console.log(a.next()) // {value:undefined,done:true}

Generator 可迭代

每个generator 对象都有 next方法,这意味着它是可迭代的(iterable),他内置了iterable接口。

javascript
1function* generateSequence() {
2  yield 1
3  yield 2
4  return 3
5}
6const a = generateSequence()
7
8for (let item of a) {
9  console.log(item)
10}
11// 1
12// 2

使用 for 循环时,会默认调用 next()方法,然后打印出value

....上面的例子中,并没有返回 3,这是因为当done为 true时,for..of循环会忽略 value

如果我们希望能够获得3 ,则必须使用 yield返回

javascript
1function* generateSequence() {
2  yield 1 //{value:1,done:false}
3  yield 2 //{value:2,done:false}
4  yield 3 //{value:3,done:false}
5}
6const a = generateSequence()
7
8for (let item of a) {
9  console.log(item)
10}
11// 1
12// 2
13// 3

由于 generator对象部署了iterable接口,所以我们可以使用展开符去展开它

javascript
1function* generateSequence() {
2  yield 1 //{value:1,done:false}
3  yield 2 //{value:2,done:false}
4  yield 3 //{value:3,done:false}
5}
6const a = generateSequence()
7const arr = [...a]
8console.log(arr) // [1, 2, 3]

使用 Generator 迭代

下面我们为对象手写一个 iterable 接口

javascript
1let range = {
2  from: 1,
3  to: 5,
4  // for..of range 在一开始就调用一次这个方法
5  [Symbol.iterator]() {
6    // ...它返回 iterator object:
7    // 后续的操作中,for..of 将只针对这个对象,并使用 next() 向它请求下一个值
8    return {
9      current: this.from,
10      last: this.to,
11      // for..of 循环在每次迭代时都会调用 next()
12      next() {
13        // 它应该以对象 {done:.., value :...} 的形式返回值
14        if (this.current <= this.L) {
15          return {
16            value: this.current++,
17            done: false,
18          }
19        }
20        return {
21          done: true,
22        }
23      },
24    }
25  },
26}
27
28for (let item of range) {
29  console.log(item) // 1 2 3 4 5
30}

上面代码主要做了如下工作

  • 给对象设置[Symbol.iterator]属性,这个属性是一个函数
  • 该函数返回一个iterator object,拥有 next方法
  • next方法返回包含 value 和 done两个属性的对象

由于 Generator函数的语法糖可以帮助返回包含 value 和 done两个属性的对象,所以我们可以对其进行改写

javascript
1let range = {
2  from: 1,
3  to: 5,
4  *[Symbol.iterator]() {
5    for (let i = this.from; i <= this.to; i++) {
6      yield i
7    }
8  },
9}
10
11const generator = range[Symbol.iterator]()
12console.log(generator.next()) // 1
13console.log(generator.next()) // 2
14console.log(generator.next()) // 3
15console.log(generator.next()) // 4
16console.log(generator.next()) // 5

当for..of循环时,会默认调用[Symbol.iterator]的 next方法,然后取其返回对象的 value值,直到 done为true为止

Generator 函数能够帮助生成 iterator接口所需要的内容:

  • 一个拥有 next方法的对象(此时为generator 对象)
  • 该对象next方法可以返回{done:boolean,value:any}结构的对象

所以Generator可以看作 iterator接口的语法糖

Generator 组合

使用 yield *指令可以将执行委托给另一个 generator,比如下面我们使用这个语法来将一个 generator组合到另一个 generator中

javascript
1function* generateSequence(start, end) {
2  for (let i = start; i <= end; i++) yield i
3}
4
5function* generateAll() {
6  yield* generateSequence(1, 6) // *
7  for (let i = start; i <= end; i++) yield i // *行与这行代码效果是一样的
8}
9const g = generateAll()
10for (let item of g) {
11  console.log(item) // 1 2 3 4 5 6 7 8 9 10
12}

Generator 传递参数

yield不仅可以向外返回结果,还可以将外部的值传递到 generator内部。

调用 generator.next(arg),我们就可以将参数arg传递到 generator 内部,这个 arg 参数会变成yield的结果。

javascript
1function* gen() {
2  // 向外部代码传递一个问题并等待答案
3  let result = yield '2 + 2 = ?' // (*)
4
5  alert(result)
6}
7
8let generator = gen()
9
10console.log(generator.next()) // 结果为 {done: false,value: "2 + 2 = ?"}
11console.log(generator.next(4)) // 会弹出 4 但结果为 {done: true,value: undefined}

yield 会将外部传递的参数 4 传递给 result,然后通过弹框打印出来。此时 yield 已执行结束,所以返回给外部的 yield 值为{done: true,value: undefined}

image-20211018173631723

  • 第一次调用generator.next()时,会经过 yield,此时往外部返回 yield值。(如果此时传递 arg 参数,那么会被忽略)它往外部返回第一个yield结果 {done: false,value: "2 + 2 = ?"}
  • 此时,在第*行,generator 执行暂停
  • 当调用generator.next(4)时,generator 恢复,并获取外部传入的 4 作为result:let result = 4

我们可以发现,generator 是通过yield来向外部传递值,外部则通过调用 next方法向 generator函数内部传递参数来交换结果。

根据上面的结论,我们再来看一个例子

javascript
1function* gen() {
2  let ask1 = yield '2 + 2 = ?'
3
4  alert(ask1) // 4
5
6  let ask2 = yield '3 * 3 = ?'
7
8  alert(ask2) // 9
9}
10
11let generator = gen()
12
13console.log('yield', generator.next()) //yield {done: false,value: "2 + 2 = ?"}
14console.log('yield', generator.next(4)) //alert 4 yield {done: false,value: "3 * 3 = ?"}
15console.log('yield', generator.next(9)) //alert 9 yield {done: true,value: undefined}

我们可以发现:

  • 一共有两个 yield 关键字,那么如果希望传递给外部的 yield有 value,则需要调用两次 next方法
  • 当调用第二次 next并传递arg时,会把arg当成结果传递给 ask1。然后发现后面还有 yield,就又往外传递。此时就相当于内外部互相传递了信息
  • 当调用第三次 next时,由于没有yield关键字了,所以就只是向内部传递一下arg,往外透出的是{done: true,value: undefined}

执行图:

image-20211018180128813

  1. 第一个 .next() 启动了 generator 的执行……执行到达第一个 yield。
  2. 结果被返回到外部代码中。
  3. 第二个 .next(4) 将 4 作为第一个 yield 的结果传递回 generator 并恢复 generator 的执行。
  4. ……执行到达第二个 yield,它变成了 generator.next(4) 调用的结果。
  5. 第三个 next(9) 将 9 作为第二个 yield 的结果传入 generator 并恢复 generator 的执行,执行现在到达了函数的最底部,所以返回 done: true。

这个过程就像“乒乓球”游戏。每个 next(value)(除了第一个)传递一个值到 generator 中,该值变成了当前 yield 的结果,然后获取下一个 yield 的结果。

generator.throw

外部代码可以通过generator.next方法将值传递给 generator,作为yield的结果,也可以在yield的代码行抛出一个 error,

如果想这样做,我们需要调用generator.throw来将 error抛到对应的yield所在行

javascript
1function* gen() {
2  try {
3    let result = yield '2 + 2 = ?' // (1)
4
5    alert('执行没有到这里,因为上面抛出了异常')
6  } catch (e) {
7    alert(e) // 显示这个 error
8  }
9}
10
11let generator = gen()
12
13generator.next()
14
15generator.throw(new Error('这是一个错误')) // (2)
  • 当第一次调用generator.next()时,会在第(1)行yield一个结果出来,然后暂停
  • 外部调用generator.throw将错误抛给内部时,此时generator继续执行,执行到 let result,并被try..catch捕获到,抛给外部一个错误,后面的代码不会被继续执行

如果我们没有在generator内部捕获它,那么这个错误就会从 generator掉出到外部代码中。

javascript
1function* gen() {
2  let result = yield '2 + 2 = ?' // (1)
3  alert('执行没有到这里,因为上面抛出了异常')
4}
5
6let generator = gen()
7try {
8  generator.next()
9  generator.throw(new Error('这是一个错误')) // (2)
10} catch (error) {
11  alert(error)
12}

下面的代码效果是一样的。

小结

  • Generator 函数通过 function*关键字创建
  • 在 generator 内部,使用 yield关键字
  • 外部和generator内部可以通过next/yield调换结果

异步迭代

迭代器是采用 iterator接口的,如果希望给一个未内置迭代接口的对象设置iterator 接口,我们需要完成以下步骤:

  1. 给对象设置[Symbol.iterator]属性,这个属性指向一个 function
  2. 该 function需要返回一个具有 next方法的对象,我们称之为iterator object
  3. 该对象的 next方法返回具有 done:boolean和value:any属性的对象

以下是例子

javascript
1let range = {
2  from: 1,
3  to: 5,
4  //for..of 循环开始时,会调用这个方法,获取 iterator object
5  [Symbol.iterator]() {
6    return {
7      current: this.from,
8      last: this.to,
9      // for..of 每次循环,都会调用 next 方法,直到 done 为 true 为止
10      next() {
11        return this.current <= this.last
12          ? // next 方法返回的对象必须为{done:boolean,value:any}的格式
13            { done: false, value: this.current++ }
14          : { done: true }
15      },
16    }
17  },
18}
19for (let item of range) console.log(item) // 1 2 3 4 5

如果我们希望能够采用异步迭代,比如说 setTimeout 的延迟之后迭代,就需要用到异步迭代。

要让上面的对象拥有异步迭代的能力,我们需要做以下调整

  • [Symbol.iterator]修改为[Symbol.asyncIterator]
  • next方法返回一个 Promise,并且状态为 fulfilled
  • 使用 for await ..进行循环

下面是修改后的代码

javascript
1let range = {
2  from: 1,
3  to: 5,
4  [Symbol.asyncIterator]() {
5    return {
6      current: this.from,
7      last: this.to,
8      // 这里的 async 是让return 的值变成 promise
9      async next() {
10        // 这里使用 await 阻塞,模拟延迟效果
11        await new Promise((resolve) => setTimeout(resolve, 1000))
12        return this.current <= this.last ? { done: false, value: this.current++ } : { done: true }
13      },
14    }
15  },
16}
17// 配合 for await 必须用 async
18;(async () => {
19  for await (let item of range) {
20    console.log(item)
21  }
22})()
  • next 方法可以不是async,这里只是为了让 next必须返回promise而写的语法糖
  • 为了让异步迭代生效,其内部必须具有Symbol.asyncIterator方法
  • 使用 for await 迭代,需要用上 async 关键字

以下是两者对比差异的表格:

Iterator异步 iterator
提供 iterator 的对象方法Symbol.iteratorSymbol.asyncIterator
next() 返回的值是任意值Promise
要进行循环,使用for..offor await..of

异步 generator

generator 可以很方便地帮助我们生成 iterator 所需要的东西:

——通过*关键字生成generator对象,内部包含 next方法

——通过 yield抛出 具有done和value属性的对象

上面的代码可以修改成这样

javascript
1let range = {
2  from: 1,
3  to: 5,
4  async *[Symbol.asyncIterator]() {
5    for (let i = this.from; i <= this.to; i++) {
6      await new Promise((resolve) => setTimeout(resolve, 1000))
7      yield i
8    }
9  },
10}
11// 配合 for await 必须用 async
12;(async () => {
13  for await (let item of range) {
14    console.log(item) // 1 2 3 4 5
15  }
16})()

此时,generator.next()返回一个 promise,且它是一个异步的函数

javascript
1;(async () => {
2  const asyncGenerator = range[Symbol.asyncIterator]()
3  const result = await asyncGenerator.next() // 返回Promise Object {done:false,value:1}
4  console.log(result) // {done:false,value:1}
5})()