在 ES6 標(biāo)準(zhǔn)中诀豁,提供了 Generator 函數(shù)(即“生成器函數(shù)”)倘核,它是一種異步編程的解決方案窿撬。在前面一篇文章中也提到一二吩跋。
一撇叁、Generator 簡(jiǎn)述
避免有人混淆概念,先說明一下:
生成器對(duì)象常被我們稱為“生成器”(Generator)劲藐,而 Generator 函數(shù)常稱為“生成器函數(shù)”(Generator Function)八堡。
由于生成器對(duì)象是實(shí)現(xiàn)了可迭代協(xié)議和迭代器協(xié)議的,因此生成器也是一個(gè)迭代器聘芜,生成器也是一個(gè)可迭代對(duì)象兄渺。所以,本文有時(shí)候直接稱為迭代器汰现,其實(shí)指的就是生成器對(duì)象挂谍。
// 生成器函數(shù)
function* genFn() {}
// 生成器對(duì)象
const gen = genFn()
// 生成器對(duì)象包含 @@iterator 方法,因此滿足可迭代協(xié)議
gen[Symbol.iterator] // ? [Symbol.iterator]() { [native code] }
// 生成器對(duì)象含 next 方法瞎饲,因此也是滿足迭代器協(xié)議的
gen.next // ? next() { [native code] }
// 生成器對(duì)象的 @@iterator 方法返回自身(即迭代器)
gen === gen[Symbol.iterator]() // true
怎樣理解 Generator 函數(shù)口叙?
Generator 函數(shù)是一個(gè)狀態(tài)機(jī),封裝了多個(gè)內(nèi)部狀態(tài)嗅战。
Generator 函數(shù)返回一個(gè)生成器對(duì)象庐扫,該對(duì)象也實(shí)現(xiàn)了 Iterator 接口(也可供
for...of
等消費(fèi)使用),所以具有了next()
方法。因此形庭,使得生成器對(duì)象擁有了開始、暫停和恢復(fù)代碼執(zhí)行的能力厌漂。生成器對(duì)象可以用于自定義迭代器和實(shí)現(xiàn)協(xié)程(coroutine)萨醒。
Generator 函數(shù)從字面理解,形式與普通函數(shù)很相似苇倡。在函數(shù)名稱前面加一個(gè)星號(hào)(
*
)富纸,表示它是一個(gè)生成器函數(shù)。盡管語(yǔ)法上與普通函數(shù)相似旨椒,但語(yǔ)法行為卻完全不同晓褪。Generator 函數(shù)強(qiáng)大之處,感覺很多人沒 GET 到综慎。它可以在不同階段從外部直接向內(nèi)部注入不同的值來調(diào)整函數(shù)的行為涣仿。
生成器對(duì)象,是由 Generator 函數(shù)返回的示惊,并且它返回可迭代協(xié)議和迭代器協(xié)議好港,因此生成器對(duì)象是一個(gè)可迭代對(duì)象。
倘若對(duì)迭代器 Iterator 不熟悉的話米罚,建議先看下這篇文章:細(xì)讀 ES6 之 Iterator 迭代器钧汹,以熟悉相關(guān)內(nèi)容。
二录择、Generator 函數(shù)語(yǔ)法
1. Generator 函數(shù)
與普通函數(shù)聲明類似拔莱,但有兩個(gè)特有特征:
- 一個(gè)是
function
關(guān)鍵字與函數(shù)名稱之間有一個(gè)星號(hào)*
; - 二是函數(shù)體內(nèi)使用
yield
表達(dá)式隘竭,以定義不同的內(nèi)部狀態(tài)塘秦。
星號(hào)
*
位置沒有明確限制,只要處于關(guān)鍵字與函數(shù)名之間即可货裹,空格可用可無嗤形,不影響。還有弧圆,這里yield
是“產(chǎn)出”的意思赋兵。
實(shí)際中,基本上使用字面量形式去聲明一個(gè) Generator 函數(shù)搔预,很少用到構(gòu)造函數(shù) GeneratorFunction 來聲明的霹期。
例如,先來一個(gè)最簡(jiǎn)單的示例拯田。
// generatorFn 是一個(gè)生成器函數(shù)
function* generatorFn() {
console.log('do something...')
// other statements
}
// 調(diào)用生成器函數(shù)历造,返回一個(gè)生成器對(duì)象。
const gen = generatorFn()
// 注意,上面像平常一樣調(diào)用函數(shù)吭产,并不會(huì)執(zhí)行函數(shù)體內(nèi)部的邏輯/語(yǔ)句侣监。
// 需要(不斷地)調(diào)用生成器對(duì)象的 next() 方法,才會(huì)開始(繼續(xù))執(zhí)行內(nèi)部的語(yǔ)句臣淤。
// 具體如何執(zhí)行橄霉,請(qǐng)看下一個(gè)示例。
gen.next()
// 執(zhí)行到這里邑蒋,才會(huì)打印出:"do something..."
// 且 gen.next() 的返回值是:{ value: undefined, done: true }
上述示例中姓蜂,調(diào)用生成器函數(shù)被調(diào)用,并不會(huì)立即立即執(zhí)行函數(shù)體內(nèi)部的語(yǔ)句医吊。另外钱慢,函數(shù)體內(nèi)的 yield
表達(dá)式是可選的,可以不寫卿堂,但這就失去了生成器函數(shù)本身的意義了束莫。
再看示例:
function* generatorFn() {
console.log(1)
yield '100'
console.log(2)
yield '200'
console.log(3)
return '300'
}
const gen = generatorFn()
前面提到,Generator 函數(shù)返回一個(gè)生成器御吞,它也是一個(gè)迭代器麦箍。因此生成器內(nèi)部存在一個(gè)指針對(duì)象,指向每次遍歷結(jié)束的位置陶珠。每調(diào)用生成器的 next()
方法挟裂,指針對(duì)象會(huì)從函數(shù)頭部(首次調(diào)用時(shí))或上一次停下來的地方開始執(zhí)行,直到遇到下一個(gè) yield
表達(dá)式(或 return
語(yǔ)句)為止揍诽。
上面一共調(diào)用了四次 next()
方法诀蓉,從結(jié)果分析:
當(dāng)首次調(diào)用 gen.next()
方法,代碼執(zhí)行到 yield '100'
會(huì)停下來(指針對(duì)象指向此處)暑脆,并返回一個(gè) IteratorResult
對(duì)象:{ value: '100', done: false }
渠啤,包含 done
和 value
屬性。其中 value
屬性值就是 yield
表達(dá)式的返回值 '100'
添吗,done
為 false
表示遍歷還沒結(jié)束沥曹。
第二次調(diào)用 next()
方法,它會(huì)從上次 yield
表達(dá)式停下的地方開始執(zhí)行碟联,直到下一個(gè) yield
表達(dá)式(指針對(duì)象也會(huì)指向此處)妓美,并返回 IteratorResult
對(duì)象:{ value: '200', done: false }
第三次調(diào)用 next()
方法,執(zhí)行過程同理鲤孵。它遇到 return
語(yǔ)句遍歷就結(jié)束了壶栋。返回 IteratorResult
對(duì)象為:{ value: '300', done: true }
,其中 value
對(duì)應(yīng) return
表達(dá)式的返回值普监。如果 Generator 函數(shù)內(nèi)沒有 return
語(yǔ)句贵试,那么 value
屬性值為 undefined
琉兜,因此返回 { value: undefined, done: true }
。
第四次調(diào)用 next() 方法毙玻,返回 { value: undefined, done: true }
豌蟋,原因是生成器對(duì)象 gen
已遍歷結(jié)束。當(dāng)?shù)饕驯闅v結(jié)束淆珊,無論你再調(diào)用多少次 next()
方法夺饲,都是返回這個(gè)值。
2. yield 表達(dá)式
生成器函數(shù)返回的迭代器對(duì)象施符,只有調(diào)用 next()
方法才會(huì)遍歷下一個(gè)內(nèi)部狀態(tài),所以它提供了一種可以暫停執(zhí)行的函數(shù)擂找。而 yield
表達(dá)式就是暫停標(biāo)志戳吝。
遍歷器對(duì)象的 next()
方法的運(yùn)行邏輯如下:
(1)遇到
yield
表達(dá)式,就暫停執(zhí)行后面的操作贯涎,并將緊跟在yield
后面的那個(gè)表達(dá)式的值听哭,作為返回的對(duì)象的value
屬性值。(2)下一次調(diào)用
next()
方法時(shí)塘雳,再繼續(xù)往下執(zhí)行陆盘,直到遇到下一個(gè)yield
表達(dá)式。(3)如果沒有再遇到新的
yield
表達(dá)式败明,就一直運(yùn)行到函數(shù)結(jié)束隘马,直到return
語(yǔ)句為止,并將return
語(yǔ)句后面的表達(dá)式的值妻顶,作為返回的對(duì)象的value
屬性值酸员。(4)如果該函數(shù)沒有
return
語(yǔ)句,則返回的對(duì)象的value
屬性值為undefined
讳嘱。
需要注意的是幔嗦,yield
表達(dá)式后面的表達(dá)式,只有在調(diào)用 next()
方法沥潭,且內(nèi)部指針指向該語(yǔ)句時(shí)才會(huì)執(zhí)行邀泉,因此相當(dāng)于為 JavaScript 提供了手動(dòng)的“惰性求值”(Lazy Evaluation)的語(yǔ)法功能。
function* generatorFn() {
// 請(qǐng)注意 yield 關(guān)鍵字后面的表達(dá)式钝鸽,是惰性求值的汇恤!
// 為了更明顯地說明問題,這里使用 IIFE寞埠。
yield (function () {
console.log('here here')
return 1
})()
}
const gen = generatorFn()
gen.next() // 調(diào)用 next 方法才會(huì)打印出:"here here"
上面的示例中屁置,yield
后面的立即執(zhí)行函數(shù)表達(dá)式,不會(huì)在調(diào)用 generatorFn()
后立即求值仁连,只會(huì)在調(diào)用 gen.next()
方法才會(huì)進(jìn)行求值蓝角。
3. yield 與 return 的特點(diǎn)及異同點(diǎn)
無論普通函數(shù)還是 Generator 函數(shù)阱穗,最多只能有一個(gè)
return
語(yǔ)句,表示該函數(shù)的終止使鹅。若沒有顯式聲明揪阶,相當(dāng)于在函數(shù)體最后return undefined
。
yield
表達(dá)式患朱,只能在 Generator 函數(shù)內(nèi)使用鲁僚,否則會(huì)報(bào)錯(cuò)。一個(gè) Generator 函數(shù)中裁厅,可以有多個(gè)
yield
語(yǔ)句冰沙。每個(gè)yield
語(yǔ)句對(duì)應(yīng)生成器的一個(gè)狀態(tài)。
yield
表達(dá)式具備“記憶”功能执虹,而return
是不具備的拓挥。每當(dāng)遇到yield
,函數(shù)暫停執(zhí)行袋励,下一次再?gòu)脑撐恢美^續(xù)向后執(zhí)行侥啤。它是由迭代器內(nèi)部由一個(gè)(指針)對(duì)象去維護(hù)的,我們無需關(guān)心茬故。Generator 函數(shù)內(nèi)部可以不用
yield
表達(dá)式盖灸。但如果這樣使用 Generator 函數(shù)就沒意義了,不如考慮使用普通函數(shù)磺芭。理論上赁炎,
yield
表達(dá)式可以返回任何值。若語(yǔ)句僅有yield;
徘跪,相當(dāng)于yield undefined;
甘邀。
4. yield 注意點(diǎn)
請(qǐng)注意以下幾點(diǎn),否則可能會(huì)出現(xiàn)語(yǔ)法錯(cuò)誤垮庐。
// ? 1. yield 只能用在 Generator 函數(shù)里面
function* foo() {
[1].map(item => {
yield item // SyntaxError
// 這里 Array.prototype.map() 的回調(diào)函數(shù)松邪,并不是一個(gè)生成器函數(shù)
})
}
// ? 2. 當(dāng) yield 表達(dá)式作用于另外一個(gè)表達(dá)式,必須放入圓括號(hào)里面
function* foo() {
// Wrong
// console.log('Hello' + yield) // SyntaxError
// console.log('Hello' + yield 'World') // SyntaxError
// Correct
console.log('Hello ' + (yield))
console.log('Hello ' + (yield 'World'))
// 不過要注意的是哨查,(多次)調(diào)用生成器實(shí)例的 next() 方法
// 以上兩個(gè)都會(huì)打印出 "Hello undefined"逗抑,并不是想象中的 "Hello World"。
// yield 表達(dá)式本身沒有返回值寒亥,或者說總是返回 undefined邮府,
// yield 關(guān)鍵字后面的表達(dá)式結(jié)果,只會(huì)作為 IteratorResult 對(duì)象的 value 值溉奕。
}
// ? yield 表達(dá)式可以用作函數(shù)參數(shù)褂傀,或放在表達(dá)式的右邊,可以不加括號(hào)
function* foo() {
const bar = (a, b) => {
console.log('paramA:', a)
console.log('paramB:', b)
}
bar(yield 'AAA', yield 'BBB')
let input = yield
return input
// 多次調(diào)用 next 方法加勤,bar 函數(shù)中仙辟,只會(huì)打印出:"paramA: undefined"同波、"paramB: undefined"
// 原因第 2 點(diǎn)提到過了
}
Generator 函數(shù)還可以這樣用:
// 函數(shù)聲明形式
function* generatorFn() {}
// 函數(shù)表達(dá)式形式
const generatorFn = function* () {}
// 作為對(duì)象屬性
const obj = {
* generatorFn() {} // or
// generatorFn: function* () {}
}
// 作為類的實(shí)例方法,或類的靜態(tài)方法
class Foo {
static * generatorFn() {}
* generatorFn() {}
}
三叠国、Generator 應(yīng)用詳解
前面提到的只是生成器函數(shù)的語(yǔ)法與簡(jiǎn)單用法未檩,并沒有體現(xiàn)其強(qiáng)大之處冀宴。
1. Generator 與 Iterator
生成器里面是部署了 Iterator 接口鲫剿,因此可以把它當(dāng)做迭代器供 for...of
等使用佑女。前面一篇文章提到斋射,使用生成器函數(shù)來實(shí)現(xiàn)自定義迭代器。
看示例:
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
表達(dá)式本身沒有返回值,或者說總是返回 undefined
翅阵。next()
方法可以帶一個(gè)參數(shù),該參數(shù)被作為上一個(gè) yield
表達(dá)式的返回值迁央。
function* generatorFn() {
let str = 'Hello ' + (yield 'World')
console.log(str)
return str
}
const gen1 = generatorFn()
const gen2 = generatorFn()
不傳遞參數(shù)時(shí)掷匠,執(zhí)行結(jié)果如下:
// 第一次調(diào)用 next()
console.log(gen1.next())
// 打印出:{ done: false, value: 'World' }
// 第二次調(diào)用 next()
console.log(gen1.next())
// "Hello undefined"
// { done: true, value: 'Hello undefined' }
相信剛開始學(xué) Generator 的童鞋,會(huì)認(rèn)為在第二次調(diào)用 gen1.next()
方法時(shí)岖圈,str
變量的值會(huì)變成 'Hello World'
讹语,當(dāng)初我也是這么認(rèn)為的,但這是錯(cuò)誤的蜂科,str
的值 'Hello undefined'
顽决。
yield
關(guān)鍵字后面的表達(dá)式結(jié)果,僅作為 next()
方法的返回對(duì)象 IteratorResult
的 value
屬性值导匣,即:{ done: false, value: 'World' }
才菠。
但如果我們?cè)?next()
方法進(jìn)行傳參呢?
// 第一次調(diào)用 next()
console.log(gen2.next('Invalid'))
// 打印出:{ done: false, value: 'World' }
// 第二次調(diào)用 next()
console.log(gen2.next('JavaScript'))
// "Hello JavaScript"
// { done: true, value: 'Hello JavaScript' }
需要注意的是贡定,由于
next()
方法表示上一個(gè)yield
表達(dá)式的返回值赋访,因此在第一次使用next()
方法時(shí),傳遞的參數(shù)是無效的缓待。只有第二次(起)調(diào)用next()
方法蚓耽,參數(shù)才有效。從語(yǔ)義上講旋炒,第一個(gè)next()
方法用于啟動(dòng)遍歷器對(duì)象步悠,所以不用帶有參數(shù)。
第一次調(diào)用 gen2.next('Invalid')
時(shí)瘫镇,參數(shù) 'Invalid'
是無效的鼎兽,所以結(jié)果還是 { done: false, value: 'World' }
答姥。
當(dāng)?shù)诙握{(diào)用 gen2.next('JavaScript')
時(shí),由于該參數(shù)將作為上一次 yield
表達(dá)式的返回值接奈。所以 let str = 'Hello ' + (yield 'World')
就相當(dāng)于 let str = 'Hello ' + 'JavaScript'
踢涌,因此 str
就變成了 'Hello JavaScript'
,自然 gen2.next()
的返回值就是 { done: true, value: 'Hello JavaScript' }
序宦。
這個(gè)功能有很重要的語(yǔ)法意義睁壁。Generator 函數(shù)從暫停狀態(tài)到恢復(fù)運(yùn)行,它的上下文狀態(tài)(context)是不變的互捌。通過給
next()
方法傳遞參數(shù)潘明,就有辦法在 Generator 函數(shù)開始運(yùn)行之后,繼續(xù)向函數(shù)體內(nèi)部注入值秕噪。也就是說钳降,可以在 Generator 函數(shù)運(yùn)行的不同階段,從外部向內(nèi)部注入不同的值腌巾,從而調(diào)整函數(shù)行為遂填。
如果還沒弄懂,再看一個(gè)示例:
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 }
// 若結(jié)果跟你內(nèi)心預(yù)期的一樣澈蝙,那說明你弄明白了吓坚!
如果想在第一次調(diào)用 next()
方法時(shí)傳入?yún)?shù)并使其有效。換個(gè)思路就行:在 Generator 函數(shù)外面包裹一個(gè)函數(shù)灯荧,在此函數(shù)內(nèi)部調(diào)用第一次礁击,并返回生成器即可。
function genWrapper(genFn) {
return function (...args) {
const g = genFn(...args)
g.next() // 其實(shí)是在內(nèi)部調(diào)用了真正意義上的第一次 next 方法逗载。
return g
}
}
function* generatorFn() {
let str = 'Hello ' + (yield 'World')
console.log(str)
return str
}
const gen = genWrapper(generatorFn)(5)
// 這樣在外部調(diào)用 next() 就算是“第一次”
gen.next('JavaScript') // { done: true, value: 'Hello JavaScript' }
3. for...of 語(yǔ)句
for...of
語(yǔ)句是 ES6 標(biāo)準(zhǔn)新增的一種循環(huán)遍歷的的方式哆窿,為了 Iterator 而生的。只有任何部署了 Iterator 接口的對(duì)象厉斟,都可以使用它來遍歷挚躯。
那 for...of 什么時(shí)候會(huì)停止循環(huán)呢?
我們知道 for...of
內(nèi)部其實(shí)是不斷調(diào)用迭代器 next()
的過程捏膨,當(dāng) next()
方法返回的 IteratorResult
對(duì)象的 done
屬性為 true
時(shí)秧均,循環(huán)就會(huì)中止,且不包含返回對(duì)象号涯。
請(qǐng)看示例和注釋:
function* generatorFn() {
yield 1
yield 2
yield 3
yield 4
yield 5
return 6 // 一般不指定 return 語(yǔ)句
}
const gen = generatorFn()
for (const x of gen) {
console.log(x)
}
// 依次打幽亢:1、2链快、3誉己、4、5
console.log([...gen]) // 打印結(jié)果為:[]
// ?
// 一般情況下域蜗,迭代器是不指定 return 語(yǔ)句的巨双,即返回 return undefined噪猾,
// 因?yàn)橛龅?return 時(shí),調(diào)用 next 會(huì)返回:{ done: true, value: '對(duì)應(yīng)return的結(jié)果' }
// 這時(shí)無論使用 for...of筑累,還是數(shù)組解構(gòu)或其他袱蜡,它們看到狀態(tài) done 為 true(表示遍歷結(jié)束),
// 它們就停止往下遍歷了慢宗,而且不會(huì)遍歷 { done: true } 的這一次哦坪蚁!
// 所以,示例中 for...of 只會(huì)打印出 0 ~ 5镜沽,而不包括 6敏晤。
// 同理,執(zhí)行到 [...gen] 時(shí)缅茉,由于此前迭代器已經(jīng)是 done: true 結(jié)束狀態(tài)嘴脾,
// 因此解構(gòu)結(jié)果就是一個(gè)空數(shù)組:[]
此前的文章提到過,迭代器是一次性對(duì)象蔬墩,而且不應(yīng)該重用生成器译打。例如上面示例中,已經(jīng)使用 for...of
去遍歷完 gen
對(duì)象了拇颅,然后還使用解構(gòu)去遍歷 gen
對(duì)象扶平,由于解構(gòu)之前 gen
對(duì)象已結(jié)束,再去使用就沒意義了蔬蕊。
再看示例,你就明白了:
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
或是解構(gòu)操作们妥,遇到狀態(tài) done
為 true
就會(huì)中止猜扮,且不包含返回對(duì)象。
for...of
本質(zhì)上就是一個(gè) while
循環(huán)监婶。
const arr = [1, 2, 3]
for (const x of arr) {
console.log(x)
}
// 相當(dāng)于
const iter = arr[Symbol.iterator]() // 迭代器
let iterRes = iter.next() // IteratorResult
while (!iterRes.done) { // 當(dāng) done 為 true 時(shí)退出循環(huán)
console.log(iterRes.value)
iterRes = iter.next()
}
建議:同一個(gè)迭代器最好不要重復(fù)使用旅赢。
4. Generator.prototype.return()
此前的文章提到過,迭代器要提前退出惑惶,并“關(guān)閉”迭代器(即狀態(tài) done
變?yōu)?true
)煮盼,需要實(shí)現(xiàn)迭代器協(xié)議的 return()
方法。
也提到過带污,生成器對(duì)象本身實(shí)現(xiàn)了 return()
方法僵控。因此,因應(yīng)不同場(chǎng)景鱼冀,使用 break
报破、continue
悠就、return
、throw
或數(shù)組解構(gòu)未消費(fèi)所有值時(shí)充易,都會(huì)提前關(guān)閉狀態(tài)梗脾。
function* foo() {
yield 1
yield 2
console.log('here')
yield 3
}
// 情況一:屬于未消費(fèi)所有值,也會(huì)提前關(guān)閉盹靴。其中 x 為 1, y 為 2炸茧。
const [x, y] = foo()
// 情況二:使用 break 提前退出,因此不會(huì)執(zhí)行到 console.log('here') 這條語(yǔ)句鹉究。
for (const x of foo()) {
console.log(x)
if (x === 2) break
}
// 依次打佑盍ⅰ:1、2
// 情況三:屬于從開始到結(jié)尾自赔,迭代完全
for (const x of foo()) {
console.log(x)
}
// 依次打勇栲凇:1、2绍妨、"here"润脸、3
對(duì)于生成器對(duì)象,除了通過以上方式“提前關(guān)閉”之外他去,還提供了一個(gè) 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 不傳參時(shí),value 為 undefined灾测。
gen.next() // { done: true, value: undefined }
注意爆价,return()
方法的參數(shù)是可選的。當(dāng)傳遞某個(gè)參數(shù)時(shí)媳搪,它將作為 { done: true, value: '參數(shù)對(duì)應(yīng)的值' }
铭段。若不傳參,那么 value
的值為 undefined
秦爆。
但如果 Generator 函數(shù)體內(nèi)序愚,包含 try...finally
代碼塊,且正在執(zhí)行 try
代碼塊等限,那么 return()
方法會(huì)導(dǎo)致立即進(jìn)入 finally
代碼塊爸吮,執(zhí)行完以后,整個(gè)函數(shù)才會(huì)結(jié)束望门。
function* foo() {
yield 1
try {
yield 2
yield 3
} finally {
yield 4
yield 5
}
yield 6
}
// ? 注意執(zhí)行順序及結(jié)果
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' }
上面代碼中形娇,調(diào)用 return()
方法后,就開始執(zhí)行 finally
代碼塊筹误,不執(zhí)行 try
里面剩下的代碼了埂软,然后等到 finally
代碼塊執(zhí)行完,再返回 return()
方法指定的返回值。
5. Generator.prototype.throw()
生成器對(duì)象都有一個(gè) throw()
方法(注意勘畔,它跟全局的 throw
關(guān)鍵字是兩回事)所灸,可以在函數(shù)體外拋出錯(cuò)誤,然后在 Generator 函數(shù)體內(nèi)捕獲炫七。
當(dāng)生成器未開始之前或者已結(jié)束(已關(guān)閉)之后爬立,調(diào)用生成器的
throw()
方法。它的錯(cuò)誤信息會(huì)被生成器函數(shù)外部的try...catch
捕獲到万哪。若外部沒有try...catch
語(yǔ)句侠驯,則會(huì)報(bào)錯(cuò)且代碼就會(huì)停止執(zhí)行。
未開始奕巍,是指調(diào)用 Generator 函數(shù)返回生成器對(duì)象之后吟策,第一次就調(diào)用了
throw()
方法。此時(shí)由于 Generator 函數(shù)還沒開始執(zhí)行的止,throw()
方法拋出的錯(cuò)誤只能拋出到 Generator 函數(shù)外檩坚。已結(jié)束,是指生成器對(duì)象的狀態(tài)是
{ done: true }
诅福。此后再調(diào)用生成器對(duì)象throw()
方法匾委,錯(cuò)誤只能在 Generator 函數(shù)外被捕獲。以上兩種情況均不會(huì)被 Generator 函數(shù)內(nèi)部的
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"
上面示例中赂乐,當(dāng)代碼執(zhí)行到 gen.throw('a')
時(shí)(此前已調(diào)用過一次 gen.next()
了),由于 Generator 函數(shù)體內(nèi)部署了 try...catch
語(yǔ)句塊咖气,因此在外部的 gen.throw('a')
會(huì)被內(nèi)部的 catch
捕獲到挨措,而且參數(shù) 'a'
將作為 catch
語(yǔ)句塊的參數(shù),所以打印出 'Generator Inner: a'
崩溪。
請(qǐng)注意运嗜,當(dāng) throw()
方法被捕獲到之后,會(huì)“自動(dòng)”執(zhí)行下一條 yield
表達(dá)式悯舟,相當(dāng)于調(diào)用一次 next()
方法。由于 Generator 函數(shù)體內(nèi)在執(zhí)行 catch
之后砸民,已經(jīng)沒有其他語(yǔ)句抵怎,相當(dāng)于有一個(gè)隱式的 return undefined
,即 gen
對(duì)象會(huì)變成 done
為 true
而關(guān)閉岭参。所以 console.log(gen.throw('a'))
就會(huì)打印出 { value: undefined, done: true }
反惕。
完了繼續(xù)執(zhí)行 gen.throw('b')
方法,由于 gen
已經(jīng)是“結(jié)束狀態(tài)”演侯,所以 throw()
方法拋出的錯(cuò)誤將會(huì)在 Generator 函數(shù)外部被捕獲到姿染。所以就是打印出:'Generator Outer: b'
。
怕有人還沒完全理解,再給出一個(gè)示例:
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()
之后狡汉,內(nèi)部會(huì)自動(dòng)執(zhí)行一次 next()
方法,即執(zhí)行到 yield 2
闽颇,因此返回的 IteratorResult
對(duì)象為:{ value: 2, done: false }
盾戴。接著再執(zhí)行一次 gen.next()
方法生成器就會(huì)變成關(guān)閉狀態(tài)。
這種函數(shù)體內(nèi)捕獲錯(cuò)誤的機(jī)制兵多,大大方便了對(duì)錯(cuò)誤的處理尖啡。多個(gè)
yield
表達(dá)式,可以只用一個(gè)try...catch
代碼塊來捕獲錯(cuò)誤剩膘。如果使用回調(diào)函數(shù)的寫法衅斩,想要捕獲多個(gè)錯(cuò)誤,就不得不為每個(gè)函數(shù)內(nèi)部寫一個(gè)錯(cuò)誤處理語(yǔ)句怠褐,現(xiàn)在只在 Generator 函數(shù)內(nèi)部寫一次try...catch
語(yǔ)句就可以了畏梆。
還有,當(dāng) Generator 函數(shù)內(nèi)報(bào)錯(cuò)惫搏,且未被捕獲具温,生成器就會(huì)變成“關(guān)閉”狀態(tài)。若后續(xù)再次調(diào)用此生成器的 next()
方法筐赔,只會(huì)返回 { done: true, value: undefined }
結(jié)果铣猩。
6. next、return茴丰、throw 的共同點(diǎn)
其實(shí) next()
达皿、return()
、throw()
三個(gè)方法本質(zhì)上都是同一事件贿肩,可以放在一起理解峦椰。它們的作用都是讓 Generator 函數(shù)恢復(fù)執(zhí)行,兵器使用不同的語(yǔ)句替換 yield
表達(dá)式汰规。
const gen = function* (x, y) {
const result = yield x + y
return result
}(1, 2)
gen.next() // { done: false, value: 3 }
next()
方法是將 yield
表達(dá)式替換成一個(gè)值汤功。注意,首次調(diào)用 next()
方法進(jìn)行傳參是無效的溜哮,從第二次起才有效滔金。
gen.next(10) // { done: true, value: 10 }
// 如果第二次調(diào)用 next 方法,且不傳參時(shí)茂嗓,yield 表達(dá)式返回值為 undefined餐茵。因此,
// gen.next() // { done: true, value: undefined }
return()
方法是將 yield
表達(dá)式替換成一個(gè) return
語(yǔ)句
gen.return('closed') // { done: true, value: 'closed' }
// 這樣的話 `let result = yield x + y` 相當(dāng)于變成 `let result = return 'closed'`
throw()
方法是將 yield
表達(dá)式替換成一個(gè) throw
語(yǔ)句述吸,以主動(dòng)拋出錯(cuò)誤忿族。
gen.throw(new Error('exception')) // 報(bào)錯(cuò):Uncaught Error: exception
// 這樣的話 `let result = yield x + y` 相當(dāng)于變成 `let result = throw new Error('exception')`
7. yield* 表達(dá)式
如果在 Generator 函數(shù)內(nèi)部調(diào)用另外一個(gè) Generator 函數(shù),需要前者的函數(shù)體內(nèi)部“手動(dòng)”完成遍歷。
function* foo() {
yield 'foo1'
yield 'foo2'
// return 'something'
// 假設(shè)指定一個(gè) return 語(yǔ)句道批,
// 使用 yield* foo() 迭代時(shí)將不會(huì)被迭代到错英,
// 因此可以理解成 yield* 內(nèi)部執(zhí)行了一遍 for...of 循環(huán)。
// 返回值 something屹徘,僅當(dāng) let result = yield* foo() 使用時(shí)走趋,作為 result 的結(jié)果。
}
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 函數(shù)簿煌,在 bar
內(nèi)部調(diào)用 foo
,需要“手動(dòng)”迭代 foo
的生成器實(shí)例鉴吹。如果存在多個(gè) Generator 函數(shù)嵌套時(shí)姨伟,寫起來就會(huì)非常麻煩。
針對(duì)這種情況豆励,ES6 提供了 yield*
表達(dá)式夺荒,用于在一個(gè) Generator 函數(shù)里面執(zhí)行另外一個(gè) Generator 函數(shù)。
因此良蒸,上面的示例可以利用 yield*
改寫成:
function* foo() {
yield 'foo1'
yield 'foo2'
}
function* bar() {
yield 'bar1'
yield* foo()
yield 'bar2'
}
for (let x of bar()) {
console.log(x)
}
關(guān)于
yield
與yield*
的區(qū)別:
yield
關(guān)鍵字后面技扼,可以跟著一個(gè)值或表達(dá)式,其結(jié)果將作為next()
方法返回值的value
屬性值嫩痰。
yield*
后面剿吻,只能跟著一個(gè)可迭代對(duì)象(即具有 Iterator 接口的任意對(duì)象),否則會(huì)報(bào)錯(cuò)串纺。生成器本身就是迭代器丽旅,也是可迭代對(duì)象耕拷。
因此篮昧,yield*
后面除了生成器對(duì)象,還可以是以下這些可迭代對(duì)象等等砚著。
function* foo() {
yield 'foo1'
yield* [1, 2] // 數(shù)組祷蝌、字符串均屬于可迭代對(duì)象
yield [3, 4] // 未使用星號(hào)時(shí)茅撞,將會(huì)返回?cái)?shù)組
yield 'foo2'
yield* 'Hi'
yield 'JSer' // 同理,未使用星號(hào)將會(huì)返回整個(gè)字符串
// yield 100 // 若 yield* 后面跟一個(gè)不可迭代對(duì)象巨朦,將會(huì)報(bào)錯(cuò):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 函數(shù)中的 this
在普通函數(shù)中 this
指向當(dāng)前的執(zhí)行上下文環(huán)境,而箭頭函數(shù)則不存在 this
癣疟,那么 Generator 函數(shù)中 this
是怎樣的呢挣柬?
function* foo() {}
const gen = foo()
foo.prototype.sayHi = function () { console.log('Hi~') }
console.log(gen instanceof foo) // true
gen.sayHi() // "Hi~"
上面的示例中,實(shí)例 gen
繼承了 foo.prototype
睛挚。Generator 函數(shù)算是構(gòu)造函數(shù)邪蛔,但它是“特殊”的構(gòu)造函數(shù),它不返回 this
實(shí)例扎狱,而是生成器實(shí)例侧到。
function* foo() {
this.a = 1
}
const gen = foo()
gen.next()
console.log(gen.a) // undefined
// 其實(shí)我們通過打印 this 可知,this 仍指向當(dāng)前執(zhí)行上下文環(huán)境淤击。
// 此處執(zhí)行上下文環(huán)境是全局匠抗,因此 this 是 window 對(duì)象。
// 如果執(zhí)行 gen.next() 時(shí)所處的上下文是某個(gè)對(duì)象(假設(shè)為 obj)污抬,
// 那么 this 就會(huì)指向 obj汞贸,而不是 gen 對(duì)象。
// 看著是不是有點(diǎn)像以下這個(gè):
// function Bar() {
// this.a = 1
// return {} // 不返回 this印机,返回另一個(gè)對(duì)象
// }
// const bar = new Bar()
// console.log(bar.a) // undefined
上面的示例中矢腻,當(dāng)我們調(diào)用 gen.next()
方法,會(huì)給 this.a
賦值為 1
射赛,接著打印 gen.a
的結(jié)果卻是 undefined
多柑,說明 this
并不是指向 gen
生成器實(shí)例。所以咒劲,Generator 函數(shù)跟平常的構(gòu)造函數(shù)是不一樣的顷蟆。
而且,不能使用 new
關(guān)鍵字進(jìn)行實(shí)例化腐魂,會(huì)報(bào)錯(cuò)帐偎。
const gen2 = new foo() // TypeError: foo is not a constructor
9. Generator 與上下文
JavaScript 代碼運(yùn)行時(shí),會(huì)產(chǎn)生一個(gè)全局的上下文環(huán)境(context蛔屹,又稱運(yùn)行環(huán)境)削樊,包含了當(dāng)前所有的變量和對(duì)象。然后兔毒,執(zhí)行函數(shù)(或塊級(jí)代碼)的時(shí)候漫贞,又會(huì)在當(dāng)前上下文環(huán)境的上層,產(chǎn)生一個(gè)函數(shù)運(yùn)行的上下文育叁,變成當(dāng)前(active)的上下文迅脐,由此形成一個(gè)上下文環(huán)境的堆棧(context stack)。
這個(gè)堆棧是“后進(jìn)先出”的數(shù)據(jù)結(jié)構(gòu)豪嗽,最后產(chǎn)生的上下文環(huán)境首先執(zhí)行完成谴蔑,退出堆棧豌骏,然后再執(zhí)行完成它下層的上下文,直至所有代碼執(zhí)行完成隐锭,堆棧清空窃躲。
Generator 函數(shù)不是這樣,它執(zhí)行產(chǎn)生的上下文環(huán)境钦睡,一旦遇到 yield
命令蒂窒,就會(huì)暫時(shí)退出堆棧,但是并不消失荞怒,里面的所有變量和對(duì)象會(huì)凍結(jié)在當(dāng)前狀態(tài)洒琢。等到對(duì)它執(zhí)行 next
命令時(shí),這個(gè)上下文環(huán)境又會(huì)重新加入調(diào)用棧挣输,凍結(jié)的變量和對(duì)象恢復(fù)執(zhí)行纬凤。
function* foo() {
yield 1
return 2
}
let gen = foo()
console.log(
gen.next().value,
gen.next().value
)
上面代碼中,第一次執(zhí)行 gen.next()
時(shí)撩嚼,Generator 函數(shù) foo
的上下文會(huì)加入堆棧停士,即開始運(yùn)行 foo
內(nèi)部的代碼。等遇到 yield 1
時(shí)完丽,foo
上下文退出堆棧恋技,內(nèi)部狀態(tài)凍結(jié)。第二次執(zhí)行 gen.next()
時(shí)逻族,foo
上下文重新加入堆棧蜻底,變成當(dāng)前的上下文,重新恢復(fù)執(zhí)行聘鳞。
四薄辅、Generator 的應(yīng)用
Generator 與 Promise 都是 ES6 通過的異步編程的解決方案。盡管 Promise 有效解決了 ES6 之前的“回調(diào)地獄”(Callback Hell)抠璃,但它仍然需要寫一堆的 then()
或 catch()
的處理站楚。
如示例:
// 這里 delay 表示各種異步操作,比如網(wǎng)絡(luò)請(qǐng)求等等
// 一下子想不到要列舉哪些異步操作搏嗡,就用 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')
上述示例中,當(dāng)我們存在多個(gè)異步操作采盒,想利用 Promise 封裝的話旧乞,避免不了要寫一系列的 then()
或 catch()
方法,假設(shè) requestByPromise()
方法磅氨,進(jìn)行網(wǎng)絡(luò)請(qǐng)求之后尺栖,還有很多個(gè)異步操作要執(zhí)行,等它們完成之后烦租,這里封裝的 requestPromise
(請(qǐng)求操作)才會(huì)完結(jié)延赌。整個(gè)代碼的實(shí)現(xiàn)起來代碼里還是很長(zhǎng)货徙。
雖然利用
async...await
可以寫出很簡(jiǎn)潔的結(jié)構(gòu),但是本文的主角不是它皮胡。
當(dāng)然,利用 Promise.all()
等方法也可以簡(jiǎn)化以上流程赏迟。如果利用 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
}
如果像下面那樣,直接去(多次)調(diào)用 next()
方法锌杀,顯然不會(huì)得到我們預(yù)期結(jié)果甩栈,且會(huì)報(bào)錯(cuò)。
const gen = requestByGenerator('/config/user')
gen.next()
gen.next() // 這一步就會(huì)報(bào)錯(cuò)糕再,TypeError: Cannot read property 'json' of undefined
// ...
原因很簡(jiǎn)單量没,yield
表達(dá)式的返回值總是 undefined
。如果 response
要得到預(yù)期值突想,在調(diào)用 gen.next()
方法時(shí)殴蹄,應(yīng)傳入 window.fetch(url)
的結(jié)果,在下一個(gè) yield
表達(dá)式才會(huì)正確解析猾担。而且還有一個(gè)最大的問題袭灯,由于實(shí)例化 gen
對(duì)象,以及調(diào)用 gen.next()
都是同步的绑嘹,當(dāng)我們?nèi)缟鲜鍪纠{(diào)用第二次 next()
方法時(shí)稽荧,F(xiàn)etch 請(qǐng)求還沒有得到結(jié)果。即使已經(jīng)請(qǐng)求到數(shù)據(jù)工腋,但由于 Event Loop 機(jī)制姨丈,它的處理也后于 next()
方法。
請(qǐng)注意擅腰,盡管 Generator 函數(shù)是異步編程的解決方案蟋恬,但它并不是異步的,而是同步的惕鼓。只是 Generator 函數(shù)在調(diào)用之后筋现,不會(huì)立即執(zhí)行函數(shù)體內(nèi)的代碼,而是提供了
next()
等方法箱歧,方便我們?nèi)タ刂飘惒搅鞒塘T了矾飞。
因此,像前一個(gè)示例的 requestByGenerator
函數(shù)呀邢,它并不會(huì)按編寫順序“同步”地處理這些異步操作洒沦,還需要我們進(jìn)一步去封裝,才能按照預(yù)期的“同步”執(zhí)行多個(gè)異步操作价淌。
Generator 還有一個(gè)很蛋疼的問題申眼,需要主動(dòng)調(diào)用 next()
才會(huì)去執(zhí)行 Generator 函數(shù)體內(nèi)的代碼瞒津。如果利用 for...of
等語(yǔ)句去遍歷,遇到 done
為 true
的又不執(zhí)行括尸。
所以巷蚪,我們要做的就是實(shí)現(xiàn)一個(gè) Generator 執(zhí)行器。
/**
* 思路:
* 1. 封裝方法并返回一個(gè) Promise 對(duì)象濒翻;
* 2. Promise 對(duì)象的返回值就是 Generator 函數(shù)的 return 結(jié)果屁柏;
* 3. 封裝的方法內(nèi)部,要自動(dòng)調(diào)用生成器的 next() 方法有送,在生成器結(jié)束時(shí)淌喻,將結(jié)果返回 Promise 對(duì)象(fulfilled);
* 4. 這里將 Generator 內(nèi)部的異常情況雀摘,在 Generator 外部使用 try...catch 補(bǔ)換裸删,并返回 Promise 對(duì)象(rejected);
* 5. 針對(duì) Generator 函數(shù)內(nèi) yield 關(guān)鍵字后的異步操作阵赠,若非 Promise 的話涯塔,請(qǐng)使用 Promise 包裝一層;
* 6. 由于封裝方法會(huì)自動(dòng)調(diào)用 next() 方法清蚀,在 Generator 函數(shù)內(nèi)若不是異步操作伤塌,沒必要使用 yield 關(guān)鍵字去創(chuàng)建一個(gè)狀態(tài),直接同步寫法即可轧铁。
*
* @param {GeneratorFunction} genFn 生成器函數(shù)
* @param {...any} args 傳遞給生成器函數(shù)的參數(shù)
* @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!')
}
// 下面就是不斷調(diào)用 next() 方法的過程每聪,直至結(jié)束或報(bào)錯(cuò)
return new Promise((resolve, reject) => {
const gen = genFn(...args)
let iterRes = gen.next()
const goNext = iteratorResult => {
const { done, value } = iteratorResult
// Generator 結(jié)束時(shí)退出
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() 拋出異常,同時(shí)使得 gen 結(jié)束
gen.throw(err)
} catch (e) {
reject(e)
}
})
}
goNext(iterRes)
})
}
然后齿风,像下面那樣去調(diào)用即可药薯。
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 將會(huì)預(yù)期地得到 fetch 的響應(yīng)結(jié)果
})
.catch(err => {
// do something...
// 處理異常情況
})
盡管 Generator 函數(shù)提出了一種全新的異步編程的解決方案,可以在函數(shù)外部注入值取干預(yù)函數(shù)內(nèi)部的行為救斑,這種思想提供了極大的創(chuàng)造性童本,強(qiáng)大之處不是 Promise 能比的。但是在結(jié)合實(shí)際場(chǎng)景時(shí)脸候,很大可能需要自實(shí)現(xiàn)一個(gè) Generator 執(zhí)行器穷娱,使其自動(dòng)執(zhí)行生成器。
例如运沦,著名的 co 函數(shù)庫(kù)就是去做了這件事情泵额。如果想了解,可以看一下這篇文章携添,或直接看官方文檔嫁盲。但看了下 GitHub 上最新一次提交已經(jīng)是 5 年前,大概都去用 async/await
了吧烈掠。
接下來就介紹 async/await
了羞秤。