簡(jiǎn)介
基本概念
Generator
函數(shù)是ES6提供的一種異步編程解決方案,語法行為與傳統(tǒng)函數(shù)完全不同渗钉。
Generator
函數(shù)有多種理解角度郭厌。語法上,首先可以把它理解成你踩,Generator
函數(shù)是一個(gè)狀態(tài)機(jī)诅岩,封裝了多個(gè)內(nèi)部狀態(tài)。
執(zhí)行Generator
函數(shù)會(huì)返回一個(gè)遍歷器對(duì)象带膜,也就是說吩谦,Generator
函數(shù)除了狀態(tài)機(jī),還是一個(gè)遍歷器對(duì)象生成函數(shù)膝藕。返回的遍歷器對(duì)象式廷,可以依次遍歷Generator
函數(shù)內(nèi)部的每一個(gè)狀態(tài)。
形式上芭挽,Generator
函數(shù)是一個(gè)普通函數(shù)滑废,但是有兩個(gè)特征。一是袜爪,function
關(guān)鍵字與函數(shù)名之間有一個(gè)星號(hào)蠕趁;二是,函數(shù)體內(nèi)部使用yield
表達(dá)式辛馆,定義不同的內(nèi)部狀態(tài)(yield
在英語里的意思就是“產(chǎn)出”)俺陋。
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
上面代碼定義了一個(gè)Generator
函數(shù)helloWorldGenerator
,它內(nèi)部有兩個(gè)yield
表達(dá)式昙篙,即該函數(shù)有三個(gè)狀態(tài):hello
腊状,world
和return
語句。
然后瓢对,Generator
函數(shù)的調(diào)用方法與普通函數(shù)一樣寿酌。不同的是,調(diào)用Generator
函數(shù)后硕蛹,該函數(shù)并不執(zhí)行醇疼,返回的也不是函數(shù)運(yùn)行結(jié)果硕并,而是一個(gè)指向內(nèi)部狀態(tài)的指針對(duì)象,也就是遍歷器對(duì)象(Iterator Object
)秧荆。
下一步倔毙,必須調(diào)用遍歷器對(duì)象的next
方法,使得指針移向下一個(gè)狀態(tài)乙濒。也就是說陕赃,每次調(diào)用next
方法,內(nèi)部指針就從函數(shù)頭部或上一次停下來的地方開始執(zhí)行颁股,直到遇到下一個(gè)yield
表達(dá)式(或return
語句)為止。換言之诉儒,Generator
函數(shù)是分段執(zhí)行的忱反,yield
表達(dá)式是暫停執(zhí)行的標(biāo)記滤愕,而next
方法可以恢復(fù)執(zhí)行温算。
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
上面代碼一共調(diào)用了四次next
方法。
第一次調(diào)用间影,Generator
函數(shù)開始執(zhí)行注竿,直到遇到第一個(gè)yield
表達(dá)式為止。next
方法返回一個(gè)對(duì)象宇智,它的value
屬性就是當(dāng)前yield
表達(dá)式的值hello
蔓搞,done
屬性的值false
胰丁,表示遍歷還沒有結(jié)束随橘。
第二次調(diào)用,Generator
函數(shù)從上次yield
表達(dá)式停下的地方锦庸,一直執(zhí)行到下一個(gè)yield
表達(dá)式机蔗。next
方法返回的對(duì)象的value
屬性就是當(dāng)前yield
表達(dá)式的值world
,done
屬性的值false
甘萧,表示遍歷還沒有結(jié)束萝嘁。
第三次調(diào)用,Generator
函數(shù)從上次yield
表達(dá)式停下的地方牙言,一直執(zhí)行到return
語句(如果沒有return
語句卑硫,就執(zhí)行到函數(shù)結(jié)束)。next
方法返回的對(duì)象的value
屬性,就是緊跟在return
語句后面的表達(dá)式的值(如果沒有return
語句障陶,則value
屬性的值為undefined
),done
屬性的值true
,表示遍歷已經(jīng)結(jié)束侄刽。
第四次調(diào)用杂彭,此時(shí)Generator
函數(shù)已經(jīng)運(yùn)行完畢,next
方法返回對(duì)象的value
屬性為undefined
主胧,done
屬性為true
。以后再調(diào)用next
方法,返回的都是這個(gè)值囤官。
總結(jié)一下,調(diào)用Generator
函數(shù)见坑,返回一個(gè)遍歷器對(duì)象,代表Generator
函數(shù)的內(nèi)部指針。以后鲫骗,每次調(diào)用遍歷器對(duì)象的next
方法,就會(huì)返回一個(gè)有著value
和done
兩個(gè)屬性的對(duì)象。value
屬性表示當(dāng)前的內(nèi)部狀態(tài)的值排苍,是yield
表達(dá)式后面那個(gè)表達(dá)式的值;done
屬性是一個(gè)布爾值漩氨,表示是否遍歷結(jié)束款青。
ES6沒有規(guī)定饰及,function
關(guān)鍵字與函數(shù)名之間的星號(hào)腿短,寫在哪個(gè)位置赴魁。這導(dǎo)致下面的寫法都能通過。
function * foo(x, y) { ··· }
function *foo(x, y) { ··· }
function* foo(x, y) { ··· }
function*foo(x, y) { ··· }
由于Generator
函數(shù)仍然是普通函數(shù),所以一般的寫法是上面的第三種,即星號(hào)緊跟在function
關(guān)鍵字后面盔腔。
yield表達(dá)式
由于Generator
函數(shù)返回的遍歷器對(duì)象宁赤,只有調(diào)用next
方法才會(huì)遍歷下一個(gè)內(nèi)部狀態(tài)愕够,所以其實(shí)提供了一種可以暫停執(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
語句為止,并將return
語句后面的表達(dá)式的值,作為返回的對(duì)象的value
屬性值迫淹。
(4)如果該函數(shù)沒有return
語句第股,則返回的對(duì)象的value
屬性值為undefined
。
需要注意的是,yield
表達(dá)式后面的表達(dá)式,只有當(dāng)調(diào)用next
方法、內(nèi)部指針指向該語句時(shí)才會(huì)執(zhí)行冀瓦,因此等于為JavaScript提供了手動(dòng)的“惰性求值”(Lazy Evaluation)的語法功能宣鄙。
function* gen() {
yield 123 + 456;
}
上面代碼中,yield
后面的表達(dá)式123 + 456
聚谁,不會(huì)立即求值,只會(huì)在next
方法將指針移到這一句時(shí)滞诺,才會(huì)求值处嫌。
yield
表達(dá)式與return
語句既有相似之處赚楚,也有區(qū)別。相似之處在于捕虽,都能返回緊跟在語句后面的那個(gè)表達(dá)式的值泻骤。區(qū)別在于每次遇到yield
诊沪,函數(shù)暫停執(zhí)行诀诊,下一次再從該位置繼續(xù)向后執(zhí)行,而return
語句不具備位置記憶的功能蒜田。一個(gè)函數(shù)里面襟铭,只能執(zhí)行一次(或者說一個(gè))return
語句,但是可以執(zhí)行多次(或者說多個(gè))yield
表達(dá)式贱傀。正常函數(shù)只能返回一個(gè)值,因?yàn)橹荒軋?zhí)行一次return
糊探;Generator
函數(shù)可以返回一系列的值,因?yàn)榭梢杂腥我舛鄠€(gè)yield
查蓉。從另一個(gè)角度看缸匪,也可以說Generator
生成了一系列的值译暂,這也就是它的名稱的來歷(英語中,generator
這個(gè)詞是“生成器”的意思)撩炊。
Generator
函數(shù)可以不用yield
表達(dá)式外永,這時(shí)就變成了一個(gè)單純的暫緩執(zhí)行函數(shù)。
function* f() {
console.log('執(zhí)行了拧咳!')
}
var generator = f();
setTimeout(function () {
generator.next()
}, 2000);
上面代碼中伯顶,函數(shù)f
如果是普通函數(shù),在為變量generator
賦值時(shí)就會(huì)執(zhí)行骆膝。但是祭衩,函數(shù)f
是一個(gè)Generator
函數(shù),就變成只有調(diào)用next
方法時(shí)阅签,函數(shù)f
才會(huì)執(zhí)行掐暮。
另外需要注意,yield
表達(dá)式只能用在Generator
函數(shù)里面愉择,用在其他地方都會(huì)報(bào)錯(cuò)劫乱。
(function (){
yield 1;
})()
// SyntaxError: Unexpected number
上面代碼在一個(gè)普通函數(shù)中使用yield
表達(dá)式,結(jié)果產(chǎn)生一個(gè)句法錯(cuò)誤锥涕。
下面是另外一個(gè)例子衷戈。
var arr = [1, [[2, 3], 4], [5, 6]];
var flat = function* (a) {
a.forEach(function (item) {
if (typeof item !== 'number') {
yield* flat(item);
} else {
yield item;
}
});
};
for (var f of flat(arr)){
console.log(f);
}
上面代碼也會(huì)產(chǎn)生句法錯(cuò)誤,因?yàn)?code>forEach方法的參數(shù)是一個(gè)普通函數(shù)层坠,但是在里面使用了yield
表達(dá)式(這個(gè)函數(shù)里面還使用了yield*
表達(dá)式)殖妇。一種修改方法是改用for
循環(huán)。
var arr = [1, [[2, 3], 4], [5, 6]];
var flat = function* (a) {
var length = a.length;
for (var i = 0; i < length; i++) {
var item = a[i];
if (typeof item !== 'number') {
yield* flat(item);
} else {
yield item;
}
}
};
for (var f of flat(arr)) {
console.log(f);
}
// 1, 2, 3, 4, 5, 6
另外破花,yield
表達(dá)式如果用在另一個(gè)表達(dá)式之中谦趣,必須放在圓括號(hào)里面疲吸。
function* demo() {
console.log('Hello' + yield); // SyntaxError
console.log('Hello' + yield 123); // SyntaxError
console.log('Hello' + (yield)); // OK
console.log('Hello' + (yield 123)); // OK
}
yield
表達(dá)式用作函數(shù)參數(shù)或放在賦值表達(dá)式的右邊,可以不加括號(hào)前鹅。
function* demo() {
foo(yield 'a', yield 'b'); // OK
let input = yield; // OK
}
與Iterator接口的關(guān)系
上一章說過摘悴,任意一個(gè)對(duì)象的Symbol.iterator
方法,等于該對(duì)象的遍歷器生成函數(shù)舰绘,調(diào)用該函數(shù)會(huì)返回該對(duì)象的一個(gè)遍歷器對(duì)象蹂喻。
由于Generator
函數(shù)就是遍歷器生成函數(shù),因此可以把Generator
賦值給對(duì)象的Symbol.iterator
屬性捂寿,從而使得該對(duì)象具有Iterator
接口口四。
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
[...myIterable] // [1, 2, 3]
上面代碼中,Generator
函數(shù)賦值給Symbol.iterator
屬性秦陋,從而使得myIterable
對(duì)象具有了Iterator
接口蔓彩,可以被...
運(yùn)算符遍歷了。
Generator
函數(shù)執(zhí)行后驳概,返回一個(gè)遍歷器對(duì)象赤嚼。該對(duì)象本身也具有Symbol.iterator
屬性,執(zhí)行后返回自身抡句。
function* gen(){
// some code
}
var g = gen();
g[Symbol.iterator]() === g // true
上面代碼中探膊,gen
是一個(gè)Generator
函數(shù),調(diào)用它會(huì)生成一個(gè)遍歷器對(duì)象g
待榔。它的Symbol.iterator
屬性,也是一個(gè)遍歷器對(duì)象生成函數(shù)流济,執(zhí)行后返回它自己锐锣。
next方法的參數(shù)
yield
表達(dá)式本身沒有返回值,或者說總是返回undefined
绳瘟。next
方法可以帶一個(gè)參數(shù)雕憔,該參數(shù)就會(huì)被當(dāng)作上一個(gè)yield
表達(dá)式的返回值。
function* f() {
for(var i = 0; true; i++) {
var reset = yield i;
if(reset) { i = -1; }
}
}
var g = f();
g.next() // { value: 0, done: false }
g.next() // { value: 1, done: false }
g.next(true) // { value: 0, done: false }
上面代碼先定義了一個(gè)可以無限運(yùn)行的Generator
函數(shù)f
糖声,如果next
方法沒有參數(shù)斤彼,每次運(yùn)行到yield
表達(dá)式,變量reset
的值總是undefined
蘸泻。當(dāng)next
方法帶一個(gè)參數(shù)true
時(shí)琉苇,變量reset
就被重置為這個(gè)參數(shù)(即true
),因此i會(huì)等于-1悦施,下一輪循環(huán)就會(huì)從-1開始遞增并扇。
這個(gè)功能有很重要的語法意義。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) {
var y = 2 * (yield (x + 1));
var z = yield (y / 3);
return (x + y + z);
}
var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}
var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }
上面代碼中出刷,第二次運(yùn)行next
方法的時(shí)候不帶參數(shù)璧疗,導(dǎo)致y
的值等于2 * undefined
(即NaN
),除以3以后還是NaN
馁龟,因此返回對(duì)象的value
屬性也等于NaN
崩侠。第三次運(yùn)行next
方法的時(shí)候不帶參數(shù),所以z
等于undefined
坷檩,返回對(duì)象的value
屬性等于5 + NaN + undefined
却音,即NaN
。
如果向next
方法提供參數(shù)矢炼,返回結(jié)果就完全不一樣了系瓢。上面代碼第一次調(diào)用b
的next
方法時(shí),返回x+1
的值6句灌;第二次調(diào)用next
方法夷陋,將上一次yield
表達(dá)式的值設(shè)為12,因此y
等于24胰锌,返回y / 3
的值8骗绕;第三次調(diào)用next
方法,將上一次yield
表達(dá)式的值設(shè)為13资昧,因此z
等于13酬土,這時(shí)x
等于5,y
等于24格带,所以return
語句的值等于42撤缴。
注意,由于next
方法的參數(shù)表示上一個(gè)yield
表達(dá)式的返回值叽唱,所以在第一次使用next
方法時(shí)屈呕,傳遞參數(shù)是無效的。V8引擎直接忽略第一次使用next
方法時(shí)的參數(shù)尔觉,只有從第二次使用next
方法開始凉袱,參數(shù)才是有效的。從語義上講,第一個(gè)next
方法用來啟動(dòng)遍歷器對(duì)象专甩,所以不用帶有參數(shù)钟鸵。
再看一個(gè)通過next
方法的參數(shù),向Generator
函數(shù)內(nèi)部輸入值的例子涤躲。
function* dataConsumer() {
console.log('Started');
console.log(`1. ${yield}`);
console.log(`2. ${yield}`);
return 'result';
}
let genObj = dataConsumer();
genObj.next();
// Started
genObj.next('a') // 1. a
genObj.next('b') // 2. b
上面代碼是一個(gè)很直觀的例子棺耍,每次通過next
方法向Generator
函數(shù)輸入值,然后打印出來种樱。
如果想要第一次調(diào)用next
方法時(shí)蒙袍,就能夠輸入值,可以在Generator
函數(shù)外面再包一層嫩挤。
function wrapper(generatorFunction) {
return function (...args) {
let generatorObject = generatorFunction(...args);
generatorObject.next();
return generatorObject;
};
}
const wrapped = wrapper(function* () {
console.log(`First input: ${yield}`);
return 'DONE';
});
wrapped().next('hello!') // First input: hello!
上面代碼中害幅,Generator
函數(shù)如果不用wrapper
先包一層,是無法第一次調(diào)用next
方法岂昭,就輸入?yún)?shù)的以现。
for...of循環(huán)
for...of
循環(huán)可以自動(dòng)遍歷Generator
函數(shù)時(shí)生成的Iterator
對(duì)象,且此時(shí)不再需要調(diào)用next
方法约啊。
function* foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6;
}
for (let v of foo()) {
console.log(v);
}
// 1 2 3 4 5
上面代碼使用for...of
循環(huán)邑遏,依次顯示5個(gè)yield
表達(dá)式的值。這里需要注意恰矩,一旦next
方法的返回對(duì)象的done
屬性為true
记盒,for...of
循環(huán)就會(huì)中止,且不包含該返回對(duì)象外傅,所以上面代碼的return
語句返回的6纪吮,不包括在for...of
循環(huán)之中。
下面是一個(gè)利用Generator
函數(shù)和for...of
循環(huán)栏豺,實(shí)現(xiàn)斐波那契數(shù)列的例子彬碱。
function* fibonacci() {
let [prev, curr] = [0, 1];
for (;;) {
yield curr;
[prev, curr] = [curr, prev + curr];
}
}
for (let n of fibonacci()) {
if (n > 1000) break;
console.log(n);
}
從上面代碼可見,使用for...of
語句時(shí)不需要使用next
方法奥洼。
利用for...of
循環(huán),可以寫出遍歷任意對(duì)象的方法晚胡。原生的JavaScript對(duì)象沒有遍歷接口灵奖,無法使用for...of
循環(huán),通過Generator
函數(shù)為它加上這個(gè)接口估盘,就可以用了瓷患。
function* objectEntries(obj) {
let propKeys = Reflect.ownKeys(obj);
for (let propKey of propKeys) {
yield [propKey, obj[propKey]];
}
}
let jane = { first: 'Jane', last: 'Doe' };
for (let [key, value] of objectEntries(jane)) {
console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe
上面代碼中,對(duì)象jane
原生不具備Iterator
接口遣妥,無法用for...of
遍歷擅编。這時(shí),我們通過Generator
函數(shù)objectEntries
為它加上遍歷器接口,就可以用for...of
遍歷了爱态。加上遍歷器接口的另一種寫法是谭贪,將Generator
函數(shù)加到對(duì)象的Symbol.iterator
屬性上面。
function* objectEntries() {
let propKeys = Object.keys(this);
for (let propKey of propKeys) {
yield [propKey, this[propKey]];
}
}
let jane = { first: 'Jane', last: 'Doe' };
jane[Symbol.iterator] = objectEntries;
for (let [key, value] of jane) {
console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe
除了for...of
循環(huán)以外锦担,擴(kuò)展運(yùn)算符(...)俭识、解構(gòu)賦值和Array.from
方法內(nèi)部調(diào)用的,都是遍歷器接口洞渔。這意味著套媚,它們都可以將Generator
函數(shù)返回的Iterator
對(duì)象,作為參數(shù)磁椒。
function* numbers () {
yield 1
yield 2
return 3
yield 4
}
// 擴(kuò)展運(yùn)算符
[...numbers()] // [1, 2]
// Array.from 方法
Array.from(numbers()) // [1, 2]
// 解構(gòu)賦值
let [x, y] = numbers();
x // 1
y // 2
// for...of 循環(huán)
for (let n of numbers()) {
console.log(n)
}
// 1
// 2
Generator.prototype.throw()
Generator
函數(shù)返回的遍歷器對(duì)象堤瘤,都有一個(gè)throw
方法,可以在函數(shù)體外拋出錯(cuò)誤浆熔,然后在Generator
函數(shù)體內(nèi)捕獲本辐。
var g = function* () {
try {
yield;
} catch (e) {
console.log('內(nèi)部捕獲', e);
}
};
var i = g();
i.next();
try {
i.throw('a');
i.throw('b');
} catch (e) {
console.log('外部捕獲', e);
}
// 內(nèi)部捕獲 a
// 外部捕獲 b
上面代碼中,遍歷器對(duì)象i連續(xù)拋出兩個(gè)錯(cuò)誤蘸拔。第一個(gè)錯(cuò)誤被Generator
函數(shù)體內(nèi)的catch
語句捕獲师郑。i
第二次拋出錯(cuò)誤,由于Generator
函數(shù)內(nèi)部的catch
語句已經(jīng)執(zhí)行過了调窍,不會(huì)再捕捉到這個(gè)錯(cuò)誤了宝冕,所以這個(gè)錯(cuò)誤就被拋出了Generator
函數(shù)體,被函數(shù)體外的catch
語句捕獲邓萨。
throw
方法可以接受一個(gè)參數(shù)地梨,該參數(shù)會(huì)被catch
語句接收,建議拋出Error對(duì)象的實(shí)例缔恳。
var g = function* () {
try {
yield;
} catch (e) {
console.log(e);
}
};
var i = g();
i.next();
i.throw(new Error('出錯(cuò)了宝剖!'));
// Error: 出錯(cuò)了!(…)
注意歉甚,不要混淆遍歷器對(duì)象的throw
方法和全局的throw
命令万细。上面代碼的錯(cuò)誤,是用遍歷器對(duì)象的throw
方法拋出的纸泄,而不是用throw
命令拋出的赖钞。后者只能被函數(shù)體外的catch
語句捕獲。
var g = function* () {
while (true) {
try {
yield;
} catch (e) {
if (e != 'a') throw e;
console.log('內(nèi)部捕獲', e);
}
}
};
var i = g();
i.next();
try {
throw new Error('a');
throw new Error('b');
} catch (e) {
console.log('外部捕獲', e);
}
// 外部捕獲 [Error: a]
上面代碼之所以只捕獲了a
聘裁,是因?yàn)楹瘮?shù)體外的catch
語句塊雪营,捕獲了拋出的a
錯(cuò)誤以后,就不會(huì)再繼續(xù)try
代碼塊里面剩余的語句了衡便。
如果Generator
函數(shù)內(nèi)部沒有部署try...catch
代碼塊献起,那么throw
方法拋出的錯(cuò)誤洋访,將被外部try...catch
代碼塊捕獲。
var g = function* () {
while (true) {
yield;
console.log('內(nèi)部捕獲', e);
}
};
var i = g();
i.next();
try {
i.throw('a');
i.throw('b');
} catch (e) {
console.log('外部捕獲', e);
}
// 外部捕獲 a
上面代碼中谴餐,Generator
函數(shù)g
內(nèi)部沒有部署try...catch
代碼塊姻政,所以拋出的錯(cuò)誤直接被外部catch
代碼塊捕獲。
如果Generator
函數(shù)內(nèi)部和外部总寒,都沒有部署try...catch
代碼塊扶歪,那么程序?qū)?bào)錯(cuò),直接中斷執(zhí)行摄闸。
var gen = function* gen(){
yield console.log('hello');
yield console.log('world');
}
var g = gen();
g.next();
g.throw();
// hello
// Uncaught undefined
上面代碼中善镰,g.throw
拋出錯(cuò)誤以后,沒有任何try...catch
代碼塊可以捕獲這個(gè)錯(cuò)誤年枕,導(dǎo)致程序報(bào)錯(cuò)炫欺,中斷執(zhí)行。
throw
方法拋出的錯(cuò)誤要被內(nèi)部捕獲熏兄,前提是必須至少執(zhí)行過一次next
方法品洛。
function* gen() {
try {
yield 1;
} catch (e) {
console.log('內(nèi)部捕獲');
}
}
var g = gen();
g.throw(1);
// Uncaught 1
上面代碼中,g.throw(1)
執(zhí)行時(shí)摩桶,next
方法一次都沒有執(zhí)行過桥状。這時(shí),拋出的錯(cuò)誤不會(huì)被內(nèi)部捕獲硝清,而是直接在外部拋出辅斟,導(dǎo)致程序出錯(cuò)。這種行為其實(shí)很好理解芦拿,因?yàn)榈谝淮螆?zhí)行next
方法士飒,等同于啟動(dòng)執(zhí)行Generator
函數(shù)的內(nèi)部代碼,否則Generator
函數(shù)還沒有開始執(zhí)行蔗崎,這時(shí)throw
方法拋錯(cuò)只可能拋出在函數(shù)外部酵幕。
throw
方法被捕獲以后,會(huì)附帶執(zhí)行下一條yield
表達(dá)式缓苛。也就是說芳撒,會(huì)附帶執(zhí)行一次next
方法。
var gen = function* gen(){
try {
yield console.log('a');
} catch (e) {
// ...
}
yield console.log('b');
yield console.log('c');
}
var g = gen();
g.next() // a
g.throw() // b
g.next() // c
上面代碼中未桥,g.throw
方法被捕獲以后番官,自動(dòng)執(zhí)行了一次next
方法,所以會(huì)打印b
钢属。另外,也可以看到门躯,只要Generator
函數(shù)內(nèi)部部署了try...catch
代碼塊淆党,那么遍歷器的throw
方法拋出的錯(cuò)誤,不影響下一次遍歷。
另外染乌,throw
命令與g.throw
方法是無關(guān)的山孔,兩者互不影響。
var gen = function* gen(){
yield console.log('hello');
yield console.log('world');
}
var g = gen();
g.next();
try {
throw new Error();
} catch (e) {
g.next();
}
// hello
// world
上面代碼中荷憋,throw
命令拋出的錯(cuò)誤不會(huì)影響到遍歷器的狀態(tài)台颠,所以兩次執(zhí)行next
方法,都進(jìn)行了正確的操作勒庄。
這種函數(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ò)誤處理語句坛吁,現(xiàn)在只在Generator
函數(shù)內(nèi)部寫一次catch
語句就可以了。
Generator
函數(shù)體外拋出的錯(cuò)誤铐尚,可以在函數(shù)體內(nèi)捕獲拨脉;反過來,Generator
函數(shù)體內(nèi)拋出的錯(cuò)誤宣增,也可以被函數(shù)體外的catch
捕獲玫膀。
function* foo() {
var x = yield 3;
var y = x.toUpperCase();
yield y;
}
var it = foo();
it.next(); // { value:3, done:false }
try {
it.next(42);
} catch (err) {
console.log(err);
}
上面代碼中,第二個(gè)next
方法向函數(shù)體內(nèi)傳入一個(gè)參數(shù)42统舀,數(shù)值是沒有toUpperCase
方法的匆骗,所以會(huì)拋出一個(gè)TypeError
錯(cuò)誤,被函數(shù)體外的catch
捕獲誉简。
一旦Generator
執(zhí)行過程中拋出錯(cuò)誤碉就,且沒有被內(nèi)部捕獲,就不會(huì)再執(zhí)行下去了闷串。如果此后還調(diào)用next
方法瓮钥,將返回一個(gè)value
屬性等于undefined
、done
屬性等于true
的對(duì)象烹吵,即JavaScript引擎認(rèn)為這個(gè)Generator
已經(jīng)運(yùn)行結(jié)束了碉熄。
function* g() {
yield 1;
console.log('throwing an exception');
throw new Error('generator broke!');
yield 2;
yield 3;
}
function log(generator) {
var v;
console.log('starting generator');
try {
v = generator.next();
console.log('第一次運(yùn)行next方法', v);
} catch (err) {
console.log('捕捉錯(cuò)誤', v);
}
try {
v = generator.next();
console.log('第二次運(yùn)行next方法', v);
} catch (err) {
console.log('捕捉錯(cuò)誤', v);
}
try {
v = generator.next();
console.log('第三次運(yùn)行next方法', v);
} catch (err) {
console.log('捕捉錯(cuò)誤', v);
}
console.log('caller done');
}
log(g());
// starting generator
// 第一次運(yùn)行next方法 { value: 1, done: false }
// throwing an exception
// 捕捉錯(cuò)誤 { value: 1, done: false }
// 第三次運(yùn)行next方法 { value: undefined, done: true }
// caller done
上面代碼一共三次運(yùn)行next
方法,第二次運(yùn)行的時(shí)候會(huì)拋出錯(cuò)誤肋拔,然后第三次運(yùn)行的時(shí)候锈津,Generator
函數(shù)就已經(jīng)結(jié)束了,不再執(zhí)行下去了凉蜂。
Generator.prototype.return()
Generator
函數(shù)返回的遍歷器對(duì)象琼梆,還有一個(gè)return
方法性誉,可以返回給定的值,并且終結(jié)遍歷 Generator
函數(shù)茎杂。
function* gen() {
yield 1;
yield 2;
yield 3;
}
var g = gen();
g.next() // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true }
g.next() // { value: undefined, done: true }
上面代碼中错览,遍歷器對(duì)象g
調(diào)用return
方法后,返回值的value
屬性就是return
方法的參數(shù)foo
煌往。并且倾哺,Generator
函數(shù)的遍歷就終止了,返回值的done
屬性為true
刽脖,以后再調(diào)用next
方法羞海,done屬性總是返回true
。
如果return
方法調(diào)用時(shí)曾棕,不提供參數(shù)扣猫,則返回值的value
屬性為undefined
。
function* gen() {
yield 1;
yield 2;
yield 3;
}
var g = gen();
g.next() // { value: 1, done: false }
g.return() // { value: undefined, done: true }
如果Generator
函數(shù)內(nèi)部有try...finally
代碼塊翘地,那么return
方法會(huì)推遲到finally
代碼塊執(zhí)行完再執(zhí)行申尤。
function* numbers () {
yield 1;
try {
yield 2;
yield 3;
} finally {
yield 4;
yield 5;
}
yield 6;
}
var g = numbers();
g.next() // { value: 1, done: false }
g.next() // { value: 2, done: false }
g.return(7) // { value: 4, done: false }
g.next() // { value: 5, done: false }
g.next() // { value: 7, done: true }
上面代碼中,調(diào)用return
方法后衙耕,就開始執(zhí)行finally
代碼塊昧穿,然后等到finally
代碼塊執(zhí)行完,再執(zhí)行return
方法橙喘。
next()时鸵、throw()、return() 的共同點(diǎn)
next()
厅瞎、throw()
饰潜、return()
這三個(gè)方法本質(zhì)上是同一件事,可以放在一起理解和簸。它們的作用都是讓Generator
函數(shù)恢復(fù)執(zhí)行彭雾,并且使用不同的語句替換yield
表達(dá)式。
next()
是將yield
表達(dá)式替換成一個(gè)值锁保。
const g = function* (x, y) {
let result = yield x + y;
return result;
};
const gen = g(1, 2);
gen.next(); // Object {value: 3, done: false}
gen.next(1); // Object {value: 1, done: true}
// 相當(dāng)于將 let result = yield x + y
// 替換成 let result = 1;
上面代碼中薯酝,第二個(gè)next(1)
方法就相當(dāng)于將yield
表達(dá)式替換成一個(gè)值1。如果next
方法沒有參數(shù)爽柒,就相當(dāng)于替換成undefined
吴菠。
throw()
是將yield
表達(dá)式替換成一個(gè)throw
語句。
gen.throw(new Error('出錯(cuò)了')); // Uncaught Error: 出錯(cuò)了
// 相當(dāng)于將 let result = yield x + y
// 替換成 let result = throw(new Error('出錯(cuò)了'));
return()是將yield表達(dá)式替換成一個(gè)return語句浩村。
gen.return(2); // Object {value: 2, done: true}
// 相當(dāng)于將 let result = yield x + y
// 替換成 let result = return 2;
yield*表達(dá)式
如果在Generator
函數(shù)內(nèi)部做葵,調(diào)用另一個(gè)Generator
函數(shù),默認(rèn)情況下是沒有效果的心墅。
function* foo() {
yield 'a';
yield 'b';
}
function* bar() {
yield 'x';
foo();
yield 'y';
}
for (let v of bar()){
console.log(v);
}
// "x"
// "y"
上面代碼中蜂挪,foo
和bar
都是Generator
函數(shù)重挑,在bar
里面調(diào)用foo
,是不會(huì)有效果的棠涮。
這個(gè)就需要用到yield*
表達(dá)式,用來在一個(gè)Generator
函數(shù)里面執(zhí)行另一個(gè)Generator
函數(shù)刺覆。
function* bar() {
yield 'x';
yield* foo();
yield 'y';
}
// 等同于
function* bar() {
yield 'x';
yield 'a';
yield 'b';
yield 'y';
}
// 等同于
function* bar() {
yield 'x';
for (let v of foo()) {
yield v;
}
yield 'y';
}
for (let v of bar()){
console.log(v);
}
// "x"
// "a"
// "b"
// "y"
再來看一個(gè)對(duì)比的例子严肪。
function* inner() {
yield 'hello!';
}
function* outer1() {
yield 'open';
yield inner();
yield 'close';
}
var gen = outer1()
gen.next().value // "open"
gen.next().value // 返回一個(gè)遍歷器對(duì)象
gen.next().value // "close"
function* outer2() {
yield 'open'
yield* inner()
yield 'close'
}
var gen = outer2()
gen.next().value // "open"
gen.next().value // "hello!"
gen.next().value // "close"
上面例子中,outer2
使用了yield*
谦屑,outer1
沒使用驳糯。結(jié)果就是,outer1
返回一個(gè)遍歷器對(duì)象氢橙,outer2
返回該遍歷器對(duì)象的內(nèi)部值酝枢。
從語法角度看,如果yield
表達(dá)式后面跟的是一個(gè)遍歷器對(duì)象悍手,需要在yield
表達(dá)式后面加上星號(hào)帘睦,表明它返回的是一個(gè)遍歷器對(duì)象。這被稱為yield*
表達(dá)式坦康。
let delegatedIterator = (function* () {
yield 'Hello!';
yield 'Bye!';
}());
let delegatingIterator = (function* () {
yield 'Greetings!';
yield* delegatedIterator;
yield 'Ok, bye.';
}());
for(let value of delegatingIterator) {
console.log(value);
}
// "Greetings!
// "Hello!"
// "Bye!"
// "Ok, bye."
上面代碼中竣付,delegatingIterator
是代理者,delegatedIterator
是被代理者滞欠。由于yield* delegatedIterator
語句得到的值古胆,是一個(gè)遍歷器,所以要用星號(hào)表示筛璧。運(yùn)行結(jié)果就是使用一個(gè)遍歷器逸绎,遍歷了多個(gè)Generator
函數(shù),有遞歸的效果夭谤。
yield*
后面的Generator
函數(shù)(沒有return
語句時(shí))棺牧,等同于在Generator
函數(shù)內(nèi)部,部署一個(gè)for...of
循環(huán)沮翔。
function* concat(iter1, iter2) {
yield* iter1;
yield* iter2;
}
// 等同于
function* concat(iter1, iter2) {
for (var value of iter1) {
yield value;
}
for (var value of iter2) {
yield value;
}
}
上面代碼說明陨帆,yield*
后面的Generator
函數(shù)(沒有return
語句時(shí)),不過是for...of
的一種簡(jiǎn)寫形式采蚀,完全可以用后者替代前者疲牵。反之,在有return
語句時(shí)榆鼠,則需要用var value = yield* iterator
的形式獲取return
語句的值纲爸。
如果yield*
后面跟著一個(gè)數(shù)組,由于數(shù)組原生支持遍歷器妆够,因此就會(huì)遍歷數(shù)組成員识啦。
function* gen(){
yield* ["a", "b", "c"];
}
gen().next() // { value:"a", done:false }
上面代碼中负蚊,yield
命令后面如果不加星號(hào),返回的是整個(gè)數(shù)組颓哮,加了星號(hào)就表示返回的是數(shù)組的遍歷器對(duì)象家妆。
實(shí)際上,任何數(shù)據(jù)結(jié)構(gòu)只要有Iterator
接口冕茅,就可以被yield*
遍歷伤极。
let read = (function* () {
yield 'hello';
yield* 'hello';
})();
read.next().value // "hello"
read.next().value // "h"
上面代碼中,yield
表達(dá)式返回整個(gè)字符串姨伤,yield*
語句返回單個(gè)字符哨坪。因?yàn)樽址哂?code>Iterator接口,所以被yield*
遍歷乍楚。
如果被代理的Generator
函數(shù)有return
語句当编,那么就可以向代理它的Generator
函數(shù)返回?cái)?shù)據(jù)。
function* foo() {
yield 2;
yield 3;
return "foo";
}
function* bar() {
yield 1;
var v = yield* foo();
console.log("v: " + v);
yield 4;
}
var it = bar();
it.next() // {value: 1, done: false}
it.next() // {value: 2, done: false}
it.next() // {value: 3, done: false}
it.next();
// "v: foo"
// {value: 4, done: false}
it.next() // {value: undefined, done: true}
上面代碼在第四次調(diào)用next
方法的時(shí)候徒溪,屏幕上會(huì)有輸出忿偷,這是因?yàn)楹瘮?shù)foo
的return
語句,向函數(shù)bar
提供了返回值词渤。
再看一個(gè)例子牵舱。
function* genFuncWithReturn() {
yield 'a';
yield 'b';
return 'The result';
}
function* logReturned(genObj) {
let result = yield* genObj;
console.log(result);
}
[...logReturned(genFuncWithReturn())]
// The result
// 值為 [ 'a', 'b' ]
上面代碼中,存在兩次遍歷缺虐。第一次是擴(kuò)展運(yùn)算符遍歷函數(shù)logReturned
返回的遍歷器對(duì)象芜壁,第二次是yield*
語句遍歷函數(shù)genFuncWithReturn
返回的遍歷器對(duì)象。這兩次遍歷的效果是疊加的高氮,最終表現(xiàn)為擴(kuò)展運(yùn)算符遍歷函數(shù)genFuncWithReturn
返回的遍歷器對(duì)象慧妄。所以,最后的數(shù)據(jù)表達(dá)式得到的值等于[ 'a', 'b' ]
剪芍。但是塞淹,函數(shù)genFuncWithReturn
的return
語句的返回值The result
,會(huì)返回給函數(shù)logReturned
內(nèi)部的result
變量罪裹,因此會(huì)有終端輸出饱普。
yield*
命令可以很方便地取出嵌套數(shù)組的所有成員。
function* iterTree(tree) {
if (Array.isArray(tree)) {
for(let i=0; i < tree.length; i++) {
yield* iterTree(tree[i]);
}
} else {
yield tree;
}
}
const tree = [ 'a', ['b', 'c'], ['d', 'e'] ];
for(let x of iterTree(tree)) {
console.log(x);
}
// a
// b
// c
// d
// e
下面是一個(gè)稍微復(fù)雜的例子状共,使用yield*
語句遍歷完全二叉樹套耕。
// 下面是二叉樹的構(gòu)造函數(shù),
// 三個(gè)參數(shù)分別是左樹峡继、當(dāng)前節(jié)點(diǎn)和右樹
function Tree(left, label, right) {
this.left = left;
this.label = label;
this.right = right;
}
// 下面是中序(inorder)遍歷函數(shù)冯袍。
// 由于返回的是一個(gè)遍歷器,所以要用generator函數(shù)。
// 函數(shù)體內(nèi)采用遞歸算法康愤,所以左樹和右樹要用yield*遍歷
function* inorder(t) {
if (t) {
yield* inorder(t.left);
yield t.label;
yield* inorder(t.right);
}
}
// 下面生成二叉樹
function make(array) {
// 判斷是否為葉節(jié)點(diǎn)
if (array.length == 1) return new Tree(null, array[0], null);
return new Tree(make(array[0]), array[1], make(array[2]));
}
let tree = make([[['a'], 'b', ['c']], 'd', [['e'], 'f', ['g']]]);
// 遍歷二叉樹
var result = [];
for (let node of inorder(tree)) {
result.push(node);
}
result // ['a', 'b', 'c', 'd', 'e', 'f', 'g']
作為對(duì)象屬性的Generator函數(shù)
如果一個(gè)對(duì)象的屬性是Generator
函數(shù)峻仇,可以簡(jiǎn)寫成下面的形式业汰。
let obj = {
* myGeneratorMethod() {
···
}
};
上面代碼中鸠姨,myGeneratorMethod
屬性前面有一個(gè)星號(hào)坯屿,表示這個(gè)屬性是一個(gè)Generator
函數(shù)。
它的完整形式如下资盅,與上面的寫法是等價(jià)的调榄。
let obj = {
myGeneratorMethod: function* () {
// ···
}
};
Generator函數(shù)的this
Generator
函數(shù)總是返回一個(gè)遍歷器,ES6規(guī)定這個(gè)遍歷器是Generator
函數(shù)的實(shí)例呵扛,也繼承了Generator
函數(shù)的prototype
對(duì)象上的方法。
function* g() {}
g.prototype.hello = function () {
return 'hi!';
};
let obj = g();
obj instanceof g // true
obj.hello() // 'hi!'
上面代碼表明筐带,Generator
函數(shù)g
返回的遍歷器obj
今穿,是g
的實(shí)例,而且繼承了g.prototype
伦籍。但是蓝晒,如果把g
當(dāng)作普通的構(gòu)造函數(shù),并不會(huì)生效帖鸦,因?yàn)?code>g返回的總是遍歷器對(duì)象芝薇,而不是this
對(duì)象。
function* g() {
this.a = 11;
}
let obj = g();
obj.next();
obj.a // undefined
上面代碼中作儿,Generator
函數(shù)g
在this
對(duì)象上面添加了一個(gè)屬性a
洛二,但是obj
對(duì)象拿不到這個(gè)屬性。
Generator
函數(shù)也不能跟new
命令一起用攻锰,會(huì)報(bào)錯(cuò)晾嘶。
function* F() {
yield this.x = 2;
yield this.y = 3;
}
new F() // TypeError: F is not a constructor
上面代碼中,new
命令跟構(gòu)造函數(shù)F
一起使用娶吞,結(jié)果報(bào)錯(cuò)垒迂,因?yàn)?code>F不是構(gòu)造函數(shù)。
那么妒蛇,有沒有辦法讓Generator
函數(shù)返回一個(gè)正常的對(duì)象實(shí)例机断,既可以用next
方法,又可以獲得正常的this
绣夺?
下面是一個(gè)變通方法吏奸。首先,生成一個(gè)空對(duì)象乐导,使用call
方法綁定Generator
函數(shù)內(nèi)部的this
苦丁。這樣,構(gòu)造函數(shù)調(diào)用以后物臂,這個(gè)空對(duì)象就是Generator
函數(shù)的實(shí)例對(duì)象了旺拉。
function* F() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
var obj = {};
var f = F.call(obj);
f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}
obj.a // 1
obj.b // 2
obj.c // 3
上面代碼中产上,首先是F
內(nèi)部的this
對(duì)象綁定obj
對(duì)象,然后調(diào)用它蛾狗,返回一個(gè)Iterator
對(duì)象晋涣。這個(gè)對(duì)象執(zhí)行三次next
方法(因?yàn)?code>F內(nèi)部有兩個(gè)yield
表達(dá)式),完成F
內(nèi)部所有代碼的運(yùn)行沉桌。這時(shí)谢鹊,所有內(nèi)部屬性都綁定在obj
對(duì)象上了,因此obj
對(duì)象也就成了F
的實(shí)例留凭。
上面代碼中佃扼,執(zhí)行的是遍歷器對(duì)象f
,但是生成的對(duì)象實(shí)例是obj
蔼夜,有沒有辦法將這兩個(gè)對(duì)象統(tǒng)一呢兼耀?
一個(gè)辦法就是將obj
換成F.prototype
。
function* F() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
var f = F.call(F.prototype);
f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}
f.a // 1
f.b // 2
f.c // 3
再將F
改成構(gòu)造函數(shù)求冷,就可以對(duì)它執(zhí)行new
命令了瘤运。
function* gen() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
function F() {
return gen.call(gen.prototype);
}
var f = new F();
f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}
f.a // 1
f.b // 2
f.c // 3
含義
Generator與狀態(tài)機(jī)
Generator
是實(shí)現(xiàn)狀態(tài)機(jī)的最佳結(jié)構(gòu)。比如匠题,下面的clock
函數(shù)就是一個(gè)狀態(tài)機(jī)拯坟。
var ticking = true;
var clock = function() {
if (ticking)
console.log('Tick!');
else
console.log('Tock!');
ticking = !ticking;
}
上面代碼的clock
函數(shù)一共有兩種狀態(tài)(Tick
和Tock
),每運(yùn)行一次韭山,就改變一次狀態(tài)郁季。這個(gè)函數(shù)如果用Generator
實(shí)現(xiàn),就是下面這樣掠哥。
var clock = function* () {
while (true) {
console.log('Tick!');
yield;
console.log('Tock!');
yield;
}
};
上面的Generator
實(shí)現(xiàn)與ES5實(shí)現(xiàn)對(duì)比巩踏,可以看到少了用來保存狀態(tài)的外部變量ticking
,這樣就更簡(jiǎn)潔续搀,更安全(狀態(tài)不會(huì)被非法篡改)塞琼、更符合函數(shù)式編程的思想,在寫法上也更優(yōu)雅禁舷。Generator
之所以可以不用外部變量保存狀態(tài)彪杉,是因?yàn)樗旧砭桶艘粋€(gè)狀態(tài)信息,即目前是否處于暫停態(tài)牵咙。
Generator與協(xié)程
協(xié)程(coroutine
)是一種程序運(yùn)行的方式派近,可以理解成“協(xié)作的線程”或“協(xié)作的函數(shù)”。協(xié)程既可以用單線程實(shí)現(xiàn)洁桌,也可以用多線程實(shí)現(xiàn)渴丸。前者是一種特殊的子例程,后者是一種特殊的線程。
(1)協(xié)程與子例程的差異
傳統(tǒng)的“子例程”(subroutine
)采用堆棧式“后進(jìn)先出”的執(zhí)行方式谱轨,只有當(dāng)調(diào)用的子函數(shù)完全執(zhí)行完畢戒幔,才會(huì)結(jié)束執(zhí)行父函數(shù)。協(xié)程與其不同土童,多個(gè)線程(單線程情況下诗茎,即多個(gè)函數(shù))可以并行執(zhí)行,但是只有一個(gè)線程(或函數(shù))處于正在運(yùn)行的狀態(tài)献汗,其他線程(或函數(shù))都處于暫停態(tài)(suspended
)敢订,線程(或函數(shù))之間可以交換執(zhí)行權(quán)。也就是說罢吃,一個(gè)線程(或函數(shù))執(zhí)行到一半楚午,可以暫停執(zhí)行,將執(zhí)行權(quán)交給另一個(gè)線程(或函數(shù))尿招,等到稍后收回執(zhí)行權(quán)的時(shí)候醒叁,再恢復(fù)執(zhí)行。這種可以并行執(zhí)行泊业、交換執(zhí)行權(quán)的線程(或函數(shù)),就稱為協(xié)程啊易。
從實(shí)現(xiàn)上看吁伺,在內(nèi)存中,子例程只使用一個(gè)棧(stack
)租谈,而協(xié)程是同時(shí)存在多個(gè)棧篮奄,但只有一個(gè)棧是在運(yùn)行狀態(tài),也就是說割去,協(xié)程是以多占用內(nèi)存為代價(jià)窟却,實(shí)現(xiàn)多任務(wù)的并行。
(2)協(xié)程與普通線程的差異
不難看出呻逆,協(xié)程適合用于多任務(wù)運(yùn)行的環(huán)境夸赫。在這個(gè)意義上,它與普通的線程很相似咖城,都有自己的執(zhí)行上下文茬腿、可以分享全局變量。它們的不同之處在于宜雀,同一時(shí)間可以有多個(gè)線程處于運(yùn)行狀態(tài)切平,但是運(yùn)行的協(xié)程只能有一個(gè),其他協(xié)程都處于暫停狀態(tài)辐董。此外悴品,普通的線程是搶先式的,到底哪個(gè)線程優(yōu)先得到資源,必須由運(yùn)行環(huán)境決定苔严,但是協(xié)程是合作式的定枷,執(zhí)行權(quán)由協(xié)程自己分配。
由于JavaScript是單線程語言邦蜜,只能保持一個(gè)調(diào)用棧依鸥。引入?yún)f(xié)程以后,每個(gè)任務(wù)可以保持自己的調(diào)用棧悼沈。這樣做的最大好處贱迟,就是拋出錯(cuò)誤的時(shí)候,可以找到原始的調(diào)用棧絮供。不至于像異步操作的回調(diào)函數(shù)那樣衣吠,一旦出錯(cuò),原始的調(diào)用棧早就結(jié)束壤靶。
Generator
函數(shù)是ES6對(duì)協(xié)程的實(shí)現(xiàn)缚俏,但屬于不完全實(shí)現(xiàn)。Generator
函數(shù)被稱為“半?yún)f(xié)程”(semi-coroutine)贮乳,意思是只有Generator
函數(shù)的調(diào)用者忧换,才能將程序的執(zhí)行權(quán)還給 Generator 函數(shù)。如果是完全執(zhí)行的協(xié)程向拆,任何函數(shù)都可以讓暫停的協(xié)程繼續(xù)執(zhí)行亚茬。
如果將 Generator 函數(shù)當(dāng)作協(xié)程,完全可以將多個(gè)需要互相協(xié)作的任務(wù)寫成Generator
函數(shù)浓恳,它們之間使用yield
表達(dá)式交換控制權(quán)刹缝。
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* gen() {
yield 1;
return 2;
}
let g = gen();
console.log(
g.next().value,
g.next().value,
);
上面代碼中,第一次執(zhí)行g.next()
時(shí)驻右,Generator
函數(shù)gen
的上下文會(huì)加入堆棧什黑,即開始運(yùn)行gen
內(nèi)部的代碼。等遇到yield 1
時(shí)堪夭,gen
上下文退出堆棧愕把,內(nèi)部狀態(tài)凍結(jié)。第二次執(zhí)行g.next()
時(shí)森爽,gen
上下文重新加入堆棧恨豁,變成當(dāng)前的上下文,重新恢復(fù)執(zhí)行爬迟。
應(yīng)用
Generator
可以暫停函數(shù)執(zhí)行圣絮,返回任意表達(dá)式的值。這種特點(diǎn)使得Generator
有多種應(yīng)用場(chǎng)景雕旨。
1.異步操作的同步化表達(dá)
Generator
函數(shù)的暫停執(zhí)行的效果,意味著可以把異步操作寫在yield
表達(dá)式里面捧请,等到調(diào)用next
方法時(shí)再往后執(zhí)行凡涩。這實(shí)際上等同于不需要寫回調(diào)函數(shù)了,因?yàn)楫惒讲僮鞯暮罄m(xù)操作可以放在yield
表達(dá)式下面疹蛉,反正要等到調(diào)用next
方法時(shí)再執(zhí)行活箕。所以,Generator
函數(shù)的一個(gè)重要實(shí)際意義就是用來處理異步操作可款,改寫回調(diào)函數(shù)育韩。
function* loadUI() {
showLoadingScreen();
yield loadUIDataAsynchronously();
hideLoadingScreen();
}
var loader = loadUI();
// 加載UI
loader.next()
// 卸載UI
loader.next()
上面代碼中,第一次調(diào)用loadUI
函數(shù)時(shí)闺鲸,該函數(shù)不會(huì)執(zhí)行筋讨,僅返回一個(gè)遍歷器。下一次對(duì)該遍歷器調(diào)用next方法摸恍,則會(huì)顯示Loading
界面(showLoadingScreen
)悉罕,并且異步加載數(shù)據(jù)(loadUIDataAsynchronously
)赤屋。等到數(shù)據(jù)加載完成,再一次使用next方法壁袄,則會(huì)隱藏Loading
界面类早。可以看到嗜逻,這種寫法的好處是所有Loading
界面的邏輯涩僻,都被封裝在一個(gè)函數(shù),按部就班非常清晰栈顷。
Ajax是典型的異步操作逆日,通過Generator
函數(shù)部署Ajax操作,可以用同步的方式表達(dá)妨蛹。
function* main() {
var result = yield request("http://some.url");
var resp = JSON.parse(result);
console.log(resp.value);
}
function request(url) {
makeAjaxCall(url, function(response){
it.next(response);
});
}
var it = main();
it.next();
上面代碼的main
函數(shù)屏富,就是通過Ajax操作獲取數(shù)據(jù)⊥苈保可以看到狠半,除了多了一個(gè)yield
,它幾乎與同步操作的寫法完全一樣颤难。注意神年,makeAjaxCall
函數(shù)中的next
方法,必須加上response
參數(shù)行嗤,因?yàn)?code>yield表達(dá)式已日,本身是沒有值的,總是等于undefined
栅屏。
下面是另一個(gè)例子飘千,通過Generator
函數(shù)逐行讀取文本文件。
function* numbers() {
let file = new FileReader("numbers.txt");
try {
while(!file.eof) {
yield parseInt(file.readLine(), 10);
}
} finally {
file.close();
}
}
上面代碼打開文本文件栈雳,使用yield
表達(dá)式可以手動(dòng)逐行讀取文件护奈。
2.控制流管理
如果有一個(gè)多步操作非常耗時(shí),采用回調(diào)函數(shù)哥纫,可能會(huì)寫成下面這樣霉旗。
step1(function (value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
step4(value3, function(value4) {
// Do something with value4
});
});
});
});
采用Promise
改寫上面的代碼。
Promise.resolve(step1)
.then(step2)
.then(step3)
.then(step4)
.then(function (value4) {
// Do something with value4
}, function (error) {
// Handle any error from step1 through step4
})
.done();
上面代碼已經(jīng)把回調(diào)函數(shù)蛀骇,改成了直線執(zhí)行的形式厌秒,但是加入了大量Promise
的語法。Generator
函數(shù)可以進(jìn)一步改善代碼運(yùn)行流程擅憔。
function* longRunningTask(value1) {
try {
var value2 = yield step1(value1);
var value3 = yield step2(value2);
var value4 = yield step3(value3);
var value5 = yield step4(value4);
// Do something with value4
} catch (e) {
// Handle any error from step1 through step4
}
}
然后鸵闪,使用一個(gè)函數(shù),按次序自動(dòng)執(zhí)行所有步驟暑诸。
scheduler(longRunningTask(initialValue));
function scheduler(task) {
var taskObj = task.next(task.value);
// 如果Generator函數(shù)未結(jié)束岛马,就繼續(xù)調(diào)用
if (!taskObj.done) {
task.value = taskObj.value
scheduler(task);
}
}
注意棉姐,上面這種做法,只適合同步操作啦逆,即所有的task
都必須是同步的伞矩,不能有異步操作。因?yàn)檫@里的代碼一得到返回值夏志,就繼續(xù)往下執(zhí)行乃坤,沒有判斷異步操作何時(shí)完成。
下面沟蔑,利用for...of
循環(huán)會(huì)自動(dòng)依次執(zhí)行yield
命令的特性湿诊,提供一種更一般的控制流管理的方法。
let steps = [step1Func, step2Func, step3Func];
function* iterateSteps(steps){
for (var i=0; i< steps.length; i++){
var step = steps[i];
yield step();
}
}
上面代碼中瘦材,數(shù)組steps
封裝了一個(gè)任務(wù)的多個(gè)步驟厅须,Generator
函數(shù)iterateSteps
則是依次為這些步驟加上yield
命令。
將任務(wù)分解成步驟之后食棕,還可以將項(xiàng)目分解成多個(gè)依次執(zhí)行的任務(wù)朗和。
let jobs = [job1, job2, job3];
function* iterateJobs(jobs){
for (var i=0; i< jobs.length; i++){
var job = jobs[i];
yield* iterateSteps(job.steps);
}
}
上面代碼中,數(shù)組jobs
封裝了一個(gè)項(xiàng)目的多個(gè)任務(wù)簿晓,Generator
函數(shù)iterateJobs
則是依次為這些任務(wù)加上yield*
命令眶拉。
最后,就可以用for...of
循環(huán)一次性依次執(zhí)行所有任務(wù)的所有步驟憔儿。
for (var step of iterateJobs(jobs)){
console.log(step.id);
}
再次提醒忆植,上面的做法只能用于所有步驟都是同步操作的情況,不能有異步操作的步驟谒臼。如果想要依次執(zhí)行異步的步驟朝刊,必須使用后面的《異步操作》一章介紹的方法。
for...of
的本質(zhì)是一個(gè)while
循環(huán)蜈缤,所以上面的代碼實(shí)質(zhì)上執(zhí)行的是下面的邏輯坞古。
var it = iterateJobs(jobs);
var res = it.next();
while (!res.done){
var result = res.value;
// ...
res = it.next();
}
3.部署Iterator接口
利用Generator
函數(shù),可以在任意對(duì)象上部署Iterator
接口劫樟。
function* iterEntries(obj) {
let keys = Object.keys(obj);
for (let i=0; i < keys.length; i++) {
let key = keys[i];
yield [key, obj[key]];
}
}
let myObj = { foo: 3, bar: 7 };
for (let [key, value] of iterEntries(myObj)) {
console.log(key, value);
}
// foo 3
// bar 7
上述代碼中,myObj
是一個(gè)普通對(duì)象织堂,通過iterEntries
函數(shù)叠艳,就有了Iterator
接口。也就是說易阳,可以在任意對(duì)象上部署next
方法附较。
下面是一個(gè)對(duì)數(shù)組部署Iterator
接口的例子,盡管數(shù)組原生具有這個(gè)接口潦俺。
function* makeSimpleGenerator(array){
var nextIndex = 0;
while(nextIndex < array.length){
yield array[nextIndex++];
}
}
var gen = makeSimpleGenerator(['yo', 'ya']);
gen.next().value // 'yo'
gen.next().value // 'ya'
gen.next().done // true
4.作為數(shù)據(jù)結(jié)構(gòu)
Generator
可以看作是數(shù)據(jù)結(jié)構(gòu)拒课,更確切地說徐勃,可以看作是一個(gè)數(shù)組結(jié)構(gòu),因?yàn)?code>Generator函數(shù)可以返回一系列的值早像,這意味著它可以對(duì)任意表達(dá)式僻肖,提供類似數(shù)組的接口。
function* doStuff() {
yield fs.readFile.bind(null, 'hello.txt');
yield fs.readFile.bind(null, 'world.txt');
yield fs.readFile.bind(null, 'and-such.txt');
}
上面代碼就是依次返回三個(gè)函數(shù)卢鹦,但是由于使用了Generator
函數(shù)臀脏,導(dǎo)致可以像處理數(shù)組那樣,處理這三個(gè)返回的函數(shù)冀自。
for (task of doStuff()) {
// task是一個(gè)函數(shù)揉稚,可以像回調(diào)函數(shù)那樣使用它
}
實(shí)際上,如果用ES5表達(dá)熬粗,完全可以用數(shù)組模擬Generator
的這種用法搀玖。
function doStuff() {
return [
fs.readFile.bind(null, 'hello.txt'),
fs.readFile.bind(null, 'world.txt'),
fs.readFile.bind(null, 'and-such.txt')
];
}
上面的函數(shù),可以用一模一樣的for...of
循環(huán)處理驻呐!兩相一比較灌诅,就不難看出Generator
使得數(shù)據(jù)或者操作,具備了類似數(shù)組的接口暴氏。