1. 程式人生 > >阮一峰ES6之Generator函式理解

阮一峰ES6之Generator函式理解

Generator函式是ES6提供的一種非同步程式設計解決方案,語法行為與傳統函式完全不同。 Generator函式是一個普通函式,但是有兩個特徵。一是,function關鍵字與函式名之間有一個星號,二是,函式體內部使用yield語句,定義不同的內部狀態。
一、用法
function* helloWorldGenerator(){
yield ‘hello’;
yield ‘world’;
return ‘ending’;
}
var hw = helloWorldGenerator();
上面程式碼定義了一個 Generator 函式helloWorldGenerator,它內部有兩個yield表示式(hello和world),即該函式有三個狀態:hello,world 和 return 語句(結束執行)。
下一步,必須呼叫遍歷器物件的next方法,使得指標移向下一個狀態。也就是說,每次呼叫next方法,內部指標就從函式頭部或上一次停下來的地方開始執行,直到遇到下一個yield表示式(或return語句)為止。換言之,Generator 函式是分段執行的,yield表示式是暫停執行的標記,而next方法可以恢復執行。
hw.next()
// {value:’hello’, done:false}
hw.next()
// {value:’world’, done:false}
hw.next()
// {value:’ending’, done:false}
hw.next()
// {value:undefined, done:true}
上面程式碼一共呼叫了4次next方法。
總結一下,呼叫Generator函式,返回一個遍歷器物件,代表Generator函式的內部指標。以後每次呼叫遍歷器物件的next方法,就會返回一個有著value和done兩個屬性的物件。
value屬性表示當前的內部狀態的值,是yield語句後面那個表示式的值;done屬性是一個布林值,表示是否遍歷結束。
二、與Iterator介面的關係
由於 Generator 函式就是遍歷器生成函式,因此可以把 Generator 賦值給物件的Symbol.iterator屬性,從而使得該物件具有 Iterator 介面。
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
[…myIterable] // [1, 2, 3]
上面程式碼中,Generator 函式賦值給Symbol.iterator屬性,從而使得myIterable物件具有了 Iterator 介面,可以被…運算子遍歷了。
三、next()方法的引數
之前我們使用過next()函式,但是並沒有傳遞任何引數,其實通過傳參地一定的引數,就有辦法在Generator函式開始執行之後,繼續向函式體內部注入值。也就是說,可以在Generator函式執行的不同階段,從外部向內部注入不同的值,從而調整函式行為, 注意,由於next方法的引數表示上一個yield表示式的返回值,所以在第一次使用next方法時,傳遞引數是無效的。V8 引擎直接忽略第一次使用next方法時的引數,只有從第二次使用next方法開始,引數才是有效的。從語義上講,第一個next方法用來啟動遍歷器物件,所以不用帶有引數。
我們可以看一段程式碼:
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 }
上面程式碼中,第二次執行next方法的時候不帶引數,導致 y 的值等於2 * undefined(即NaN),除以 3 以後還是NaN,因此返回物件的value屬性也等於NaN。第三次執行Next方法的時候不帶引數,所以z等於undefined,返回物件的value屬性等於5 + NaN + undefined,即NaN。
如果向next方法提供引數,返回結果就完全不一樣了。上面程式碼第一次呼叫b的next方法時,返回x+1的值6;第二次呼叫next方法,將上一次yield表示式的值設為12,因此y等於24,返回y / 3的值8;第三次呼叫next方法,將上一次yield表示式的值設為13,因此z等於13,這時x等於5,y等於24,所以return語句的值等於42。
四、for…of迴圈
for…of迴圈可以自動遍歷 Generator 函式時生成的Iterator物件,且此時不再需要呼叫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迴圈,依次顯示 5 個yield表示式的值。這裡需要注意,一旦next方法的返回物件的done屬性為true,for…of迴圈就會中止,且不包含該返回物件,所以上面程式碼的return語句返回的6,不包括在for…of迴圈之中。
五、return()
Generator 函式返回的遍歷器物件,還有一個return方法,可以返回給定的值,並且終結遍歷 Generator 函式。
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 }
上面程式碼中,遍歷器物件g呼叫return方法後,返回值的value屬性就是return方法的引數foo。並且,Generator 函式的遍歷就終止了,返回值的done屬性為true,以後再呼叫next方法,done屬性總是返回true。如果return方法呼叫時,不提供引數,則返回值的value屬性為undefined。
六、throw()
在呼叫throw()後同樣會終止所有的yield執行,同時會丟擲一個異常,需要通過try-catch來接收:
var g = function* () {
try {
yield;
} catch (e) {
console.log(‘內部捕獲’, e);
}
};
var i = g();
i.next();
try {
i.throw(‘a’);
i.throw(‘b’);
} catch (e) {
console.log(‘外部捕獲’, e);
}
// 內部捕獲 a
// 外部捕獲 b
上面程式碼中,遍歷器物件i連續丟擲兩個錯誤。第一個錯誤被 Generator 函式體內的catch語句捕獲。i第二次丟擲錯誤,由於 Generator 函式內部的catch語句已經執行過了,不會再捕捉到這個錯誤了,所以這個錯誤就被丟擲了 Generator 函式體,被函式體外的catch語句捕獲。
七、yield*
yield*用來將一個Generator放到另一個Generator函式中執行。
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”
八、運用場景
1、代替遞迴
斐波那契數列的實現:
function * fibonacci(seed1, seed2) {
while (true) {
yield (() => {
seed2 = seed2 + seed1;
seed1 = seed2 - seed1;
return seed2;
})();
}
}
const fib = fibonacci(0, 1);
fib.next(); // {value: 1, done: false}
fib.next(); // {value: 2, done: false}
fib.next(); // {value: 3, done: false}
fib.next(); // {value: 5, done: false}
fib.next(); // {value: 8, done: false}
2、非同步操作的同步化
Generator 函式的暫停執行的效果,意味著可以把非同步操作寫在yield表示式裡面,等到呼叫next方法時再往後執行。這實際上等同於不需要寫回調函數了,因為非同步操作的後續操作可以放在yield表示式下面,反正要等到呼叫next方法時再執行。所以,Generator 函式的一個重要實際意義就是用來處理非同步操作,改寫回調函式。
Ajax 是典型的非同步操作,通過 Generator 函式部署 Ajax 操作,可以用同步的方式表達。
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函式,就是通過 Ajax 操作獲取資料。可以看到,除了多了一個yield,它幾乎與同步操作的寫法完全一樣。注意,makeAjaxCall函式中的next方法,必須加上response引數,因為yield表示式,本身是沒有值的,總是等於undefined。
逐行讀取文字檔案:
function * numbers() {
let file = new FileReader(“numbers.txt”);
try {
while(!file.eof) {
yield parseInt(file.readLine(), 10);
}
} finally {
file.close();
}
}
3、控制流的管理
如一個多步操作非常耗時,採用回撥的話:
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();
而使用generator函式:
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
}
}