在 ES6 标准中,提供了 Generator 函数(即“生成器函数”),它是一种异步编程的解决方案。在前面一篇文章中也提到一二。
一、Generator 简述
避免有人混淆概念,先说明一下:
// 生成器函数
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 函数返回的,并且它返回可迭代协议和迭代器协议,因此生成器对象是一个可迭代对象。
二、Generator 函数语法
1. Generator 函数
与普通函数声明类似,但有两个特有特征:
- 一个是
function
关键字与函数名称之间有一个星号*
; - 二是函数体内使用
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 }
,包含 done
和 value
属性。其中 value
属性值就是 yield
表达式的返回值 '100'
,done
为 false
表示遍历还没结束。
第二次调用 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()
方法的运行逻辑如下:
需要注意的是,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 的特点及异同点
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
表达式本身没有返回值,或者说总是返回 undefined
。next()
方法可以带一个参数,该参数被作为上一个 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()
方法的返回对象 IteratorResult
的 value
属性值,即:{ 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' }
第一次调用 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' }
。
如果还没弄懂,再看一个示例:
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
或是解构操作,遇到状态 done
为 true
就会中止,且不包含返回对象。
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()
方法。因此,因应不同场景,使用 break
、continue
、return
、throw
或数组解构未消费所有值时,都会提前关闭状态。
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 函数体内捕获。
看示例:
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
对象会变成 done
为 true
而关闭。所以 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()
方法生成器就会变成关闭状态。
还有,当 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"
上面示例中,foo
和 bar
都是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)
}
因此,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
(请求操作)才会完结。整个代码的实现起来代码里还是很长。
当然,利用 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()
方法。
因此,像前一个示例的 requestByGenerator
函数,它并不会按编写顺序“同步”地处理这些异步操作,还需要我们进一步去封装,才能按照预期的“同步”执行多个异步操作。
Generator 还有一个很蛋疼的问题,需要主动调用 next()
才会去执行 Generator 函数体内的代码。如果利用 for...of
等语句去遍历,遇到 done
为 true
的又不执行。
所以,我们要做的就是实现一个 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
了。