细读 ES6 | Generator 生成器

配图源自 Freepik

在 ES6 标准中,提供了 Generator 函数(即“生成器函数”),它是一种异步编程的解决方案。在前面一篇文章中也提到一二。

一、Generator 简述

避免有人混淆概念,先说明一下:

生成器对象常被我们称为“生成器”(Generator),而 Generator 函数常称为“生成器函数”(Generator Function)。

由于生成器对象是实现了可迭代协议迭代器协议的,因此生成器也是一个迭代器,生成器也是一个可迭代对象。所以,本文有时候直接称为迭代器,其实指的就是生成器对象。

// 生成器函数
function* genFn() {}

// 生成器对象
const gen = genFn()

// 生成器对象包含 @@iterator 方法,因此满足可迭代协议
gen[Symbol.iterator] // ƒ [Symbol.iterator]() { [native code] }

// 生成器对象含 next 方法,因此也是满足迭代器协议的
gen.next // ƒ next() { [native code] }

// 生成器对象的 @@iterator 方法返回自身(即迭代器)
gen === gen[Symbol.iterator]() // true

怎样理解 Generator 函数?

  • Generator 函数是一个状态机,封装了多个内部状态。

  • Generator 函数返回一个生成器对象,该对象也实现了 Iterator 接口(也可供 for...of 等消费使用),所以具有了 next() 方法。因此,使得生成器对象拥有了开始、暂停和恢复代码执行的能力。

  • 生成器对象可以用于自定义迭代器和实现协程(coroutine)。

  • Generator 函数从字面理解,形式与普通函数很相似。在函数名称前面加一个星号(*),表示它是一个生成器函数。尽管语法上与普通函数相似,但语法行为却完全不同。

  • Generator 函数强大之处,感觉很多人没 GET 到。它可以在不同阶段从外部直接向内部注入不同的值来调整函数的行为。

生成器对象,是由 Generator 函数返回的,并且它返回可迭代协议和迭代器协议,因此生成器对象是一个可迭代对象。

倘若对迭代器 Iterator 不熟悉的话,建议先看下这篇文章:细读 ES6 之 Iterator 迭代器,以熟悉相关内容。

二、Generator 函数语法

1. Generator 函数

与普通函数声明类似,但有两个特有特征:

  • 一个是 function 关键字与函数名称之间有一个星号 *
  • 二是函数体内使用 yield 表达式,以定义不同的内部状态。

星号 * 位置没有明确限制,只要处于关键字与函数名之间即可,空格可用可无,不影响。还有,这里 yield 是“产出”的意思。

实际中,基本上使用字面量形式去声明一个 Generator 函数,很少用到构造函数 GeneratorFunction 来声明的。

例如,先来一个最简单的示例。

// generatorFn 是一个生成器函数
function* generatorFn() {
  console.log('do something...')
  // other statements
}

// 调用生成器函数,返回一个生成器对象。
const gen = generatorFn()

// 注意,上面像平常一样调用函数,并不会执行函数体内部的逻辑/语句。
// 需要(不断地)调用生成器对象的 next() 方法,才会开始(继续)执行内部的语句。
// 具体如何执行,请看下一个示例。
gen.next()
// 执行到这里,才会打印出:"do something..."
// 且 gen.next() 的返回值是:{ value: undefined, done: true }

上述示例中,调用生成器函数被调用,并不会立即立即执行函数体内部的语句。另外,函数体内的 yield 表达式是可选的,可以不写,但这就失去了生成器函数本身的意义了。

再看示例:

function* generatorFn() {
  console.log(1)
  yield '100'
  console.log(2)
  yield '200'
  console.log(3)
  return '300'
}

const gen = generatorFn()

前面提到,Generator 函数返回一个生成器,它也是一个迭代器。因此生成器内部存在一个指针对象,指向每次遍历结束的位置。每调用生成器的 next() 方法,指针对象会从函数头部(首次调用时)或上一次停下来的地方开始执行,直到遇到下一个 yield 表达式(或 return 语句)为止。

上面一共调用了四次 next() 方法,从结果分析:

当首次调用 gen.next() 方法,代码执行到 yield '100' 会停下来(指针对象指向此处),并返回一个 IteratorResult 对象:{ value: '100', done: false },包含 donevalue 属性。其中 value 属性值就是 yield 表达式的返回值 '100'donefalse 表示遍历还没结束。

第二次调用 next() 方法,它会从上次 yield 表达式停下的地方开始执行,直到下一个 yield 表达式(指针对象也会指向此处),并返回 IteratorResult 对象:{ value: '200', done: false }

第三次调用 next() 方法,执行过程同理。它遇到 return 语句遍历就结束了。返回 IteratorResult 对象为:{ value: '300', done: true },其中 value 对应 return 表达式的返回值。如果 Generator 函数内没有 return 语句,那么 value 属性值为 undefined,因此返回 { value: undefined, done: true }

第四次调用 next() 方法,返回 { value: undefined, done: true },原因是生成器对象 gen 已遍历结束。当迭代器已遍历结束,无论你再调用多少次 next() 方法,都是返回这个值。

2. yield 表达式

生成器函数返回的迭代器对象,只有调用 next() 方法才会遍历下一个内部状态,所以它提供了一种可以暂停执行的函数。而 yield 表达式就是暂停标志。

遍历器对象的 next() 方法的运行逻辑如下:

(1)遇到 yield 表达式,就暂停执行后面的操作,并将紧跟在 yield 后面的那个表达式的值,作为返回的对象的 value 属性值。

(2)下一次调用next()方法时,再继续往下执行,直到遇到下一个 yield 表达式。

(3)如果没有再遇到新的 yield 表达式,就一直运行到函数结束,直到 return 语句为止,并将 return 语句后面的表达式的值,作为返回的对象的 value 属性值。

(4)如果该函数没有 return 语句,则返回的对象的 value 属性值为 undefined

需要注意的是,yield 表达式后面的表达式,只有在调用 next() 方法,且内部指针指向该语句时才会执行,因此相当于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。

function* generatorFn() {
  // 请注意 yield 关键字后面的表达式,是惰性求值的!
  // 为了更明显地说明问题,这里使用 IIFE。
  yield (function () {
    console.log('here here')
    return 1
  })()
}

const gen = generatorFn()
gen.next() // 调用 next 方法才会打印出:"here here"

上面的示例中,yield 后面的立即执行函数表达式,不会在调用 generatorFn() 后立即求值,只会在调用 gen.next() 方法才会进行求值。

3. yield 与 return 的特点及异同点
  • 无论普通函数还是 Generator 函数,最多只能有一个 return 语句,表示该函数的终止。若没有显式声明,相当于在函数体最后 return undefined

  • yield 表达式,只能在 Generator 函数内使用,否则会报错。

  • 一个 Generator 函数中,可以有多个 yield 语句。每个 yield 语句对应生成器的一个状态。

  • yield 表达式具备“记忆”功能,而 return 是不具备的。每当遇到 yield,函数暂停执行,下一次再从该位置继续向后执行。它是由迭代器内部由一个(指针)对象去维护的,我们无需关心。

  • Generator 函数内部可以不用 yield 表达式。但如果这样使用 Generator 函数就没意义了,不如考虑使用普通函数。

  • 理论上,yield 表达式可以返回任何值。若语句仅有 yield;,相当于 yield undefined;

4. yield 注意点

请注意以下几点,否则可能会出现语法错误。

// ️ 1. yield 只能用在 Generator 函数里面
function* foo() {
  [1].map(item => {
    yield item // SyntaxError
    // 这里 Array.prototype.map() 的回调函数,并不是一个生成器函数
  })
}

// ️ 2. 当 yield 表达式作用于另外一个表达式,必须放入圆括号里面
function* foo() {
  // Wrong
  // console.log('Hello' + yield) // SyntaxError
  // console.log('Hello' + yield 'World') // SyntaxError

  // Correct
  console.log('Hello ' + (yield))
  console.log('Hello ' + (yield 'World'))
  // 不过要注意的是,(多次)调用生成器实例的 next() 方法
  // 以上两个都会打印出 "Hello undefined",并不是想象中的 "Hello World"。
  // yield 表达式本身没有返回值,或者说总是返回 undefined,
  // yield 关键字后面的表达式结果,只会作为 IteratorResult 对象的 value 值。
}

// ️ yield 表达式可以用作函数参数,或放在表达式的右边,可以不加括号
function* foo() {
  const bar = (a, b) => {
    console.log('paramA:', a)
    console.log('paramB:', b)
  }
  bar(yield 'AAA', yield 'BBB')

  let input = yield
  return input
  // 多次调用 next 方法,bar 函数中,只会打印出:"paramA: undefined"、"paramB: undefined"
  // 原因第 2 点提到过了
}

Generator 函数还可以这样用:

// 函数声明形式
function* generatorFn() {}

// 函数表达式形式
const generatorFn = function* () {}

// 作为对象属性
const obj = {
  * generatorFn() {} // or
  // generatorFn: function* () {}
}

// 作为类的实例方法,或类的静态方法
class Foo {
  static * generatorFn() {}
  * generatorFn() {}
}

三、Generator 应用详解

前面提到的只是生成器函数的语法与简单用法,并没有体现其强大之处。

1. Generator 与 Iterator

生成器里面是部署了 Iterator 接口,因此可以把它当做迭代器供 for...of 等使用。前面一篇文章提到,使用生成器函数来实现自定义迭代器。

看示例:

class Counter {
  constructor([min = 0, max = 10]) {
    this.min = min
    this.max = max
  }

  *[Symbol.iterator]() {
    let point = this.min
    const end = this.max
    while (point <= end) {
      yield point++
    }
  }
}

const counter = new Counter([0, 3])
const gen = counter[Symbol.iterator]() // gen 既是生成器,又是迭代器
for (const x of gen) {
  console.log(x)
}
// 依次打印:0、1、2、3
2. next 方法传参

yield 表达式本身没有返回值,或者说总是返回 undefinednext() 方法可以带一个参数,该参数被作为上一个 yield 表达式的返回值。

function* generatorFn() {
  let str = 'Hello ' + (yield 'World')
  console.log(str)
  return str
}
const gen1 = generatorFn()
const gen2 = generatorFn()

不传递参数时,执行结果如下:

// 第一次调用 next()
console.log(gen1.next())
// 打印出:{ done: false, value: 'World' }

// 第二次调用 next()
console.log(gen1.next())
// "Hello undefined"
// { done: true, value: 'Hello undefined' }

相信刚开始学 Generator 的童鞋,会认为在第二次调用 gen1.next() 方法时,str 变量的值会变成 'Hello World',当初我也是这么认为的,但这是错误的,str 的值 'Hello undefined'

yield 关键字后面的表达式结果,仅作为 next() 方法的返回对象 IteratorResultvalue 属性值,即:{ done: false, value: 'World' }

但如果我们在 next() 方法进行传参呢?

// 第一次调用 next()
console.log(gen2.next('Invalid'))
// 打印出:{ done: false, value: 'World' }

// 第二次调用 next()
console.log(gen2.next('JavaScript'))
// "Hello JavaScript"
// { done: true, value: 'Hello JavaScript' }

需要注意的是,由于 next() 方法表示上一个 yield 表达式的返回值,因此在第一次使用 next() 方法时,传递的参数是无效的。只有第二次(起)调用 next() 方法,参数才有效。从语义上讲,第一个 next() 方法用于启动遍历器对象,所以不用带有参数。

第一次调用 gen2.next('Invalid') 时,参数 'Invalid' 是无效的,所以结果还是 { done: false, value: 'World' }

当第二次调用 gen2.next('JavaScript') 时,由于该参数将作为上一次 yield 表达式的返回值。所以 let str = 'Hello ' + (yield 'World') 就相当于 let str = 'Hello ' + 'JavaScript',因此 str 就变成了 'Hello JavaScript',自然 gen2.next() 的返回值就是 { done: true, value: 'Hello JavaScript' }

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

如果还没弄懂,再看一个示例:

function* foo(x) {
  const y = 2 * (yield (x + 1))
  const z = yield (y / 3)
  return (x + y + z)
}

const f1 = foo(5)
f1.next()     // { done: false, value: 6 }
f1.next()     // { done: false, value: NaN }
f1.next()     // { done: true, value: NaN }

const f2 = foo(5)
f2.next()     // { done: false, value: 6 }
f2.next(12)   // { done: false, value: 8 }
f2.next(13)   // { done: true, value: 42 }

// 若结果跟你内心预期的一样,那说明你弄明白了!

如果想在第一次调用 next() 方法时传入参数并使其有效。换个思路就行:在 Generator 函数外面包裹一个函数,在此函数内部调用第一次,并返回生成器即可。

function genWrapper(genFn) {
  return function (...args) {
    const g = genFn(...args)
    g.next() // 其实是在内部调用了真正意义上的第一次 next 方法。
    return g
  }
}

function* generatorFn() {
  let str = 'Hello ' + (yield 'World')
  console.log(str)
  return str
}

const gen = genWrapper(generatorFn)(5)

// 这样在外部调用 next() 就算是“第一次”
gen.next('JavaScript') // { done: true, value: 'Hello JavaScript' }

3. for...of 语句

for...of 语句是 ES6 标准新增的一种循环遍历的的方式,为了 Iterator 而生的。只有任何部署了 Iterator 接口的对象,都可以使用它来遍历。

那 for...of 什么时候会停止循环呢?

我们知道 for...of 内部其实是不断调用迭代器 next() 的过程,当 next() 方法返回的 IteratorResult 对象的 done 属性为 true 时,循环就会中止,且不包含返回对象

请看示例和注释:

function* generatorFn() {
  yield 1
  yield 2
  yield 3
  yield 4
  yield 5
  return 6 // 一般不指定 return 语句
}

const gen = generatorFn()
for (const x of gen) {
  console.log(x)
}
// 依次打印:1、2、3、4、5

console.log([...gen]) // 打印结果为:[]

// ️
// 一般情况下,迭代器是不指定 return 语句的,即返回 return undefined,
// 因为遇到 return 时,调用 next 会返回:{ done: true, value: '对应return的结果' }
// 这时无论使用 for...of,还是数组解构或其他,它们看到状态 done 为 true(表示遍历结束),
// 它们就停止往下遍历了,而且不会遍历 { done: true } 的这一次哦!
// 所以,示例中 for...of 只会打印出 0 ~ 5,而不包括 6。
// 同理,执行到 [...gen] 时,由于此前迭代器已经是 done: true 结束状态,
// 因此解构结果就是一个空数组:[]

此前的文章提到过,迭代器是一次性对象,而且不应该重用生成器。例如上面示例中,已经使用 for...of 去遍历完 gen 对象了,然后还使用解构去遍历 gen 对象,由于解构之前 gen 对象已结束,再去使用就没意义了。

再看示例,你就明白了:

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

[...foo()] // [1, 2]
Array.from(foo()) // [1, 2]
const [x, y] = foo() // x 为 1, y 为 2
for (const x of foo()) { console.log(x) } // 依次打印:1、2

所以,无论是 for...of 或是解构操作,遇到状态 donetrue 就会中止,且不包含返回对象

for...of 本质上就是一个 while 循环。

const arr = [1, 2, 3]
for (const x of arr) {
  console.log(x)
}

// 相当于
const iter = arr[Symbol.iterator]() // 迭代器
let iterRes = iter.next() // IteratorResult
while (!iterRes.done) { // 当 done 为 true 时退出循环
  console.log(iterRes.value)
  iterRes = iter.next()
}

建议:同一个迭代器最好不要重复使用。

4. Generator.prototype.return()

此前的文章提到过,迭代器要提前退出,并“关闭”迭代器(即状态 done 变为 true),需要实现迭代器协议的 return() 方法。

也提到过,生成器对象本身实现了 return() 方法。因此,因应不同场景,使用 breakcontinuereturnthrow 或数组解构未消费所有值时,都会提前关闭状态。

function* foo() {
  yield 1
  yield 2
  console.log('here')
  yield 3
}

// 情况一:属于未消费所有值,也会提前关闭。其中 x 为 1, y 为 2。
const [x, y] = foo()

// 情况二:使用 break 提前退出,因此不会执行到 console.log('here') 这条语句。
for (const x of foo()) {
  console.log(x)
  if (x === 2) break
}
// 依次打印:1、2

// 情况三:属于从开始到结尾,迭代完全
for (const x of foo()) {
  console.log(x)
}
// 依次打印:1、2、"here"、3

对于生成器对象,除了通过以上方式“提前关闭”之外,还提供了一个 Generator.prototype.return() 方法供我们使用。

function* foo() {
  yield 1
  yield 2
  yield 3
}

const gen = foo()
gen.next() // { done: false, value: 1 }
gen.return('closed') // { done: true, value: 'closed' } // 若 return 不传参时,value 为 undefined。
gen.next() // { done: true, value: undefined }

注意,return() 方法的参数是可选的。当传递某个参数时,它将作为 { done: true, value: '参数对应的值' }。若不传参,那么 value 的值为 undefined

但如果 Generator 函数体内,包含 try...finally 代码块,且正在执行 try 代码块,那么 return() 方法会导致立即进入 finally 代码块,执行完以后,整个函数才会结束。

function* foo() {
  yield 1

  try {
    yield 2
    yield 3
  } finally {
    yield 4
    yield 5
  }

  yield 6
}

// ️ 注意执行顺序及结果
const gen = foo()
gen.next()             // { done: false, value: 1 }
gen.next()             // { done: false, value: 2 }
gen.return('closed')   // { done: false, value: 4 }
gen.next()             // { done: false, value: 5 }
gen.next()             // { done: true, value: 'closed' }

上面代码中,调用 return() 方法后,就开始执行 finally 代码块,不执行 try 里面剩下的代码了,然后等到 finally 代码块执行完,再返回 return() 方法指定的返回值。

5. Generator.prototype.throw()

生成器对象都有一个 throw() 方法(注意,它跟全局的 throw 关键字是两回事),可以在函数体外抛出错误,然后在 Generator 函数体内捕获。

当生成器未开始之前或者已结束(已关闭)之后,调用生成器的 throw() 方法。它的错误信息会被生成器函数外部的 try...catch 捕获到。若外部没有 try...catch 语句,则会报错且代码就会停止执行。

  • 未开始,是指调用 Generator 函数返回生成器对象之后,第一次就调用了 throw() 方法。此时由于 Generator 函数还没开始执行,throw() 方法抛出的错误只能抛出到 Generator 函数外。

  • 已结束,是指生成器对象的状态是 { done: true }。此后再调用生成器对象 throw() 方法,错误只能在 Generator 函数外被捕获。

以上两种情况均不会被 Generator 函数内部的 try...catch 捕获到。

看示例:

function* generatorFn() {
  try {
    yield
  } catch (e) {
    console.log('Generator Inner:', e)
  }
}

const gen = generatorFn()
gen.next()

try {
  console.log(gen.throw('a'))
  console.log(gen.throw('b'))
} catch (e) {
  console.log('Generator Outer:', e)
}

// 依次打印出:
// "Generator Inner: a"
// { value: undefined, done: true }
// "Generator Outer: b"

上面示例中,当代码执行到 gen.throw('a') 时(此前已调用过一次 gen.next() 了),由于 Generator 函数体内部署了 try...catch 语句块,因此在外部的 gen.throw('a') 会被内部的 catch 捕获到,而且参数 'a' 将作为 catch 语句块的参数,所以打印出 'Generator Inner: a'

请注意,当 throw() 方法被捕获到之后,会“自动”执行下一条 yield 表达式,相当于调用一次 next() 方法。由于 Generator 函数体内在执行 catch 之后,已经没有其他语句,相当于有一个隐式的 return undefined,即 gen 对象会变成 donetrue 而关闭。所以 console.log(gen.throw('a')) 就会打印出 { value: undefined, done: true }

完了继续执行 gen.throw('b') 方法,由于 gen 已经是“结束状态”,所以 throw() 方法抛出的错误将会在 Generator 函数外部被捕获到。所以就是打印出:'Generator Outer: b'

怕有人还没完全理解,再给出一个示例:

function* generatorFn() {
  try {
    yield 1
  } catch (e) {
    console.log('Generator Inner:', e)
  }
  yield 2
}

const gen = generatorFn()
console.log(gen.next())
console.log(gen.throw(new Error('Oops')))
console.log(gen.next())
// 依次打印出:
// { value: 1, done: false }
// "Generator Inner: Error: Oops"
// { value: 2, done: false }
// { value: undefined, done: true }

以上示例中,gen.throw() 之后,内部会自动执行一次 next() 方法,即执行到 yield 2,因此返回的 IteratorResult 对象为:{ value: 2, done: false }。接着再执行一次 gen.next() 方法生成器就会变成关闭状态。

这种函数体内捕获错误的机制,大大方便了对错误的处理。多个 yield 表达式,可以只用一个 try...catch 代码块来捕获错误。如果使用回调函数的写法,想要捕获多个错误,就不得不为每个函数内部写一个错误处理语句,现在只在 Generator 函数内部写一次 try...catch 语句就可以了。

还有,当 Generator 函数内报错,且未被捕获,生成器就会变成“关闭”状态。若后续再次调用此生成器的 next() 方法,只会返回 { done: true, value: undefined } 结果。

6. next、return、throw 的共同点

其实 next()return()throw() 三个方法本质上都是同一事件,可以放在一起理解。它们的作用都是让 Generator 函数恢复执行,兵器使用不同的语句替换 yield 表达式。

const gen = function* (x, y) {
  const result = yield x + y
  return result
}(1, 2)

gen.next() // { done: false, value: 3 }

next() 方法是将 yield 表达式替换成一个值。注意,首次调用 next() 方法进行传参是无效的,从第二次起才有效。

gen.next(10) // { done: true, value: 10 }
// 如果第二次调用 next 方法,且不传参时,yield 表达式返回值为 undefined。因此,
// gen.next() // { done: true, value: undefined }

return() 方法是将 yield 表达式替换成一个 return 语句

gen.return('closed') // { done: true, value: 'closed' }
// 这样的话 `let result = yield x + y` 相当于变成 `let result = return 'closed'`

throw() 方法是将 yield 表达式替换成一个 throw 语句,以主动抛出错误。

gen.throw(new Error('exception')) // 报错:Uncaught Error: exception
// 这样的话 `let result = yield x + y` 相当于变成 `let result = throw new Error('exception')`
7. yield* 表达式

如果在 Generator 函数内部调用另外一个 Generator 函数,需要前者的函数体内部“手动”完成遍历。

function* foo() {
  yield 'foo1'
  yield 'foo2'
  // return 'something'
  // 假设指定一个 return 语句,
  // 使用 yield* foo() 迭代时将不会被迭代到,
  // 因此可以理解成 yield* 内部执行了一遍 for...of 循环。
  // 返回值 something,仅当 let result = yield* foo() 使用时,作为 result 的结果。
}

function* bar() {
  yield 'bar1'
  for (let x of foo()) {
    console.log(x)
  }
  yield 'bar2'
}

for (let x of bar()) {
  console.log(x)
}
// 依次打印出:
// "foo1"
// "bar1"
// "bar2"
// "foo2"

上面示例中,foobar 都是Generator 函数,在 bar 内部调用 foo,需要“手动”迭代 foo 的生成器实例。如果存在多个 Generator 函数嵌套时,写起来就会非常麻烦。

针对这种情况,ES6 提供了 yield* 表达式,用于在一个 Generator 函数里面执行另外一个 Generator 函数。

因此,上面的示例可以利用 yield* 改写成:

function* foo() {
  yield 'foo1'
  yield 'foo2'
}

function* bar() {
  yield 'bar1'
  yield* foo()
  yield 'bar2'
}

for (let x of bar()) {
  console.log(x)
}

关于 yieldyield* 的区别:

  • yield 关键字后面,可以跟着一个值或表达式,其结果将作为 next() 方法返回值的 value 属性值。

  • yield* 后面,只能跟着一个可迭代对象(即具有 Iterator 接口的任意对象),否则会报错。生成器本身就是迭代器,也是可迭代对象。

因此,yield* 后面除了生成器对象,还可以是以下这些可迭代对象等等。

function* foo() {
  yield 'foo1'
  yield* [1, 2] // 数组、字符串均属于可迭代对象
  yield [3, 4] // 未使用星号时,将会返回数组
  yield 'foo2'
  yield* 'Hi'
  yield 'JSer' // 同理,未使用星号将会返回整个字符串
  // yield 100 // 若 yield* 后面跟一个不可迭代对象,将会报错:TypeError: undefined is not a function
}

for (const x of foo()) {
  console.log(x)
}
// 依次打印出:"foo1"、1、2、[3, 4]、"foo2"、"H"、"i"、"JSer"
8. Generator 函数中的 this

在普通函数中 this 指向当前的执行上下文环境,而箭头函数则不存在 this,那么 Generator 函数中 this 是怎样的呢?

function* foo() {}
const gen = foo()
foo.prototype.sayHi = function () { console.log('Hi~') }

console.log(gen instanceof foo) // true
gen.sayHi() // "Hi~"

上面的示例中,实例 gen 继承了 foo.prototype。Generator 函数算是构造函数,但它是“特殊”的构造函数,它不返回 this 实例,而是生成器实例。

function* foo() {
  this.a = 1
}
const gen = foo()
gen.next()
console.log(gen.a) // undefined

// 其实我们通过打印 this 可知,this 仍指向当前执行上下文环境。
// 此处执行上下文环境是全局,因此 this 是 window 对象。
// 如果执行 gen.next() 时所处的上下文是某个对象(假设为 obj),
// 那么 this 就会指向 obj,而不是 gen 对象。

// 看着是不是有点像以下这个:
// function Bar() {
//   this.a = 1
//   return {} // 不返回 this,返回另一个对象
// }
// const bar = new Bar()
// console.log(bar.a) // undefined

上面的示例中,当我们调用 gen.next() 方法,会给 this.a 赋值为 1,接着打印 gen.a 的结果却是 undefined,说明 this 并不是指向 gen 生成器实例。所以,Generator 函数跟平常的构造函数是不一样的。

而且,不能使用 new 关键字进行实例化,会报错。

const gen2 = new foo() // TypeError: foo is not a constructor
9. Generator 与上下文

JavaScript 代码运行时,会产生一个全局的上下文环境(context,又称运行环境),包含了当前所有的变量和对象。然后,执行函数(或块级代码)的时候,又会在当前上下文环境的上层,产生一个函数运行的上下文,变成当前(active)的上下文,由此形成一个上下文环境的堆栈(context stack)。

这个堆栈是“后进先出”的数据结构,最后产生的上下文环境首先执行完成,退出堆栈,然后再执行完成它下层的上下文,直至所有代码执行完成,堆栈清空。

Generator 函数不是这样,它执行产生的上下文环境,一旦遇到 yield 命令,就会暂时退出堆栈,但是并不消失,里面的所有变量和对象会冻结在当前状态。等到对它执行 next 命令时,这个上下文环境又会重新加入调用栈,冻结的变量和对象恢复执行。

function* foo() {
  yield 1
  return 2
}

let gen = foo()

console.log(
  gen.next().value,
  gen.next().value
)

上面代码中,第一次执行 gen.next()时,Generator 函数 foo 的上下文会加入堆栈,即开始运行 foo 内部的代码。等遇到 yield 1时,foo 上下文退出堆栈,内部状态冻结。第二次执行 gen.next() 时,foo 上下文重新加入堆栈,变成当前的上下文,重新恢复执行。

四、Generator 的应用

Generator 与 Promise 都是 ES6 通过的异步编程的解决方案。尽管 Promise 有效解决了 ES6 之前的“回调地狱”(Callback Hell),但它仍然需要写一堆的 then()catch() 的处理。

如示例:

// 这里 delay 表示各种异步操作,比如网络请求等等
// 一下子想不到要列举哪些异步操作,就用 setTimeout 表示吧
// 问题不大,举例而已
function delay(time) {
  return new Promise(resolve => setTimeout(resolve, time))
}

function requestByPromise(url) {
  let result = null
  window.fetch(url)
    .then(respone => respone.json())
    .then(res => {
      result = res
    })
    .then(() => {
      return delay(1000)
    })
    .then(() => {
      return delay(2000)
    })
    .then(() => {
      return delay(3000)
    })
    .then(() => {
      console.log('Done', result)
      // do something...
    })
    .catch(err => {
      console.warn('Exception', err)
    })
}

requestByPromise('/config/user')

上述示例中,当我们存在多个异步操作,想利用 Promise 封装的话,避免不了要写一系列的 then()catch() 方法,假设 requestByPromise() 方法,进行网络请求之后,还有很多个异步操作要执行,等它们完成之后,这里封装的 requestPromise(请求操作)才会完结。整个代码的实现起来代码里还是很长。

虽然利用 async...await 可以写出很简洁的结构,但是本文的主角不是它。

当然,利用 Promise.all() 等方法也可以简化以上流程。如果利用 Generator 要怎么做呢?

如果我们想写出如下这样更直观的“同步”方式:

function* requestByGenerator(url) {
  let response = yield window.fetch(url)
  let result = yield response.json()
  yield delay(1000)
  yield delay(2000)
  yield delay(3000)
  return result
}

如果像下面那样,直接去(多次)调用 next() 方法,显然不会得到我们预期结果,且会报错。

const gen = requestByGenerator('/config/user')
gen.next()
gen.next() // 这一步就会报错,TypeError: Cannot read property 'json' of undefined
// ...

原因很简单,yield 表达式的返回值总是 undefined。如果 response 要得到预期值,在调用 gen.next() 方法时,应传入 window.fetch(url) 的结果,在下一个 yield 表达式才会正确解析。而且还有一个最大的问题,由于实例化 gen 对象,以及调用 gen.next() 都是同步的,当我们如上述示例调用第二次 next() 方法时,Fetch 请求还没有得到结果。即使已经请求到数据,但由于 Event Loop 机制,它的处理也后于 next() 方法。

请注意,尽管 Generator 函数是异步编程的解决方案,但它并不是异步的,而是同步的。只是 Generator 函数在调用之后,不会立即执行函数体内的代码,而是提供了 next() 等方法,方便我们去控制异步流程罢了。

因此,像前一个示例的 requestByGenerator 函数,它并不会按编写顺序“同步”地处理这些异步操作,还需要我们进一步去封装,才能按照预期的“同步”执行多个异步操作。

Generator 还有一个很蛋疼的问题,需要主动调用 next() 才会去执行 Generator 函数体内的代码。如果利用 for...of 等语句去遍历,遇到 donetrue 的又不执行。

所以,我们要做的就是实现一个 Generator 执行器。

/**
 * 思路:
 * 1. 封装方法并返回一个 Promise 对象;
 * 2. Promise 对象的返回值就是 Generator 函数的 return 结果;
 * 3. 封装的方法内部,要自动调用生成器的 next() 方法,在生成器结束时,将结果返回 Promise 对象(fulfilled);
 * 4. 这里将 Generator 内部的异常情况,在 Generator 外部使用 try...catch 补换,并返回 Promise 对象(rejected);
 * 5. 针对 Generator 函数内 yield 关键字后的异步操作,若非 Promise 的话,请使用 Promise 包装一层;
 * 6. 由于封装方法会自动调用 next() 方法,在 Generator 函数内若不是异步操作,没必要使用 yield 关键字去创建一个状态,直接同步写法即可。
 *
 * @param {GeneratorFunction} genFn 生成器函数
 * @param  {...any} args 传递给生成器函数的参数
 * @returns {Promise}
 */
function generatorExecutor(genFn, ...args) {
  const getType = obj => {
    const type = Object.prototype.toString.call(obj)
    return /^\[object (.*)\]$/.exec(type)[1]
  }

  if (getType(genFn) !== 'GeneratorFunction') {
    throw new TypeError('The first parameter of generatorExecutor must be a generator function!')
  }

  // 下面就是不断调用 next() 方法的过程,直至结束或报错
  return new Promise((resolve, reject) => {
    const gen = genFn(...args)
    let iterRes = gen.next()

    const goNext = iteratorResult => {
      const { done, value } = iteratorResult

      // Generator 结束时退出
      if (done) return resolve(value)

      if (getType(value) !== 'Promise') {
        const nextRes = gen.next(value)
        goNext(nextRes)
        return
      }

      // 处理 yield 为 Promise 的情况
      value.then(res => {
        const nextRes = gen.next(res)
        goNext(nextRes)
      }).catch(err => {
        try {
          // 利用 Generator.prototype.throw() 抛出异常,同时使得 gen 结束
          gen.throw(err)
        } catch (e) {
          reject(e)
        }
      })
    }

    goNext(iterRes)
  })
}

然后,像下面那样去调用即可。

function* requestByGenerator(url) {
  let response = yield window.fetch(url)
  let result = yield response.json()
  yield delay(1000)
  yield delay(2000)
  yield delay(3000)
  return result
}

generatorExecutor(requestByGenerator, '/config/user')
  .then(res => {
    // do something...
    // res 将会预期地得到 fetch 的响应结果
  })
  .catch(err => {
    // do something...
    // 处理异常情况
  })

尽管 Generator 函数提出了一种全新的异步编程的解决方案,可以在函数外部注入值取干预函数内部的行为,这种思想提供了极大的创造性,强大之处不是 Promise 能比的。但是在结合实际场景时,很大可能需要自实现一个 Generator 执行器,使其自动执行生成器。

例如,著名的 co 函数库就是去做了这件事情。如果想了解,可以看一下这篇文章,或直接看官方文档。但看了下 GitHub 上最新一次提交已经是 5 年前,大概都去用 async/await 了吧。

接下来就介绍 async/await 了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容