Generator 函數是 ES6 提供的一種異步編程解決方案,語法行爲與傳統函數完全不同
Generator 函數有多種理解角度。語法上,首先可以把它理解成,Generator 函數是一個狀態機,封裝了多個內部狀態。
執行 Generator 函數會返回一個遍歷器對象,也就是說,Generator 函數除了狀態機,還是一個遍歷器對象生成函數。返回的遍歷器對象,可以依次遍歷 Generator 函數內部的每一個狀態。
形式上,Generator 函數是一個普通函數,但是有兩個特徵。一是,function關鍵字與函數名之間有一個*
;二是,函數體內部使用yield
表達式,定義不同的內部狀態(yield在英語裏的意思就是“產出”)
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
上面代碼一共調用了四次next方法。
第一次調用,Generator
函數開始執行,直到遇到第一個yield
表達式爲止。next
方法返回一個對象,它的value
屬性就是當前yield
表達式的值hello
,done屬性的值false,表示遍歷還沒有結束
。
第二次調用,Generator
函數從上次yield
表達式停下的地方,一直執行到下一個yield
表達式。next
方法返回的對象的value
屬性就是當前yield
表達式的值world
,done屬性的值false,表示遍歷還沒有結束
。
第三次調用,Generator
函數從上次yield
表達式停下的地方,一直執行到return
語句(如果沒有return語句,就執行到函數結束)。next
方法返回的對象的value
屬性,就是緊跟在return語句後面的表達式的值(如果沒有return語句,則value屬性的值爲undefined),done屬性的值true,表示遍歷已經結束。
第四次調用,此時 Generator 函數已經運行完畢,next方法返回對象的value屬性爲undefined,done屬性爲true。以後再調用next方法,返回的都是這個值。
ES6 沒有規定,function關鍵字與函數名之間的星號,寫在哪個位置。這導致下面的寫法都能通過。
function * foo(x, y) { ··· }
function *foo(x, y) { ··· }
function* foo(x, y) { ··· }
function*foo(x, y) { ··· }
由於 Generator 函數仍然是普通函數,所以一般的寫法是上面的第三種,即星號緊跟在function關鍵字後面。本書也採用這種寫法。
yield表達式
由於 Generator 函數返回的遍歷器對象,只有調用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* gen() {
yield 123 + 456;
}
上面代碼中,yield後面的表達式123 + 456,不會立即求值,只會在next方法將指針移到這一句時,纔會求值。
next方法的參數
yield表達式本身沒有返回值,或者說總是返回undefined。next方法可以帶一個參數,該參數就會被當作上一個yield表達式的返回值。
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 }
上面代碼先定義了一個可以無限運行的 Generator
函數f
,如果next
方法沒有參數,每次運行到yield
表達式,變量reset
的值總是undefined
。當next
方法帶一個參數true
時,變量reset
就被重置爲這個參數(即true),因此i會等於-1
,下一輪循環就會從-1開始遞增。
這個功能有很重要的語法意義。Generator 函數從暫停狀態到恢復運行,它的上下文狀態(context)是不變的。通過next方法的參數,就有辦法在 Generator 函數開始運行之後,繼續向函數體內部注入值。也就是說,可以在 Generator 函數運行的不同階段,從外部向內部注入不同的值,從而調整函數行爲。
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
。
yield 表達式*
如果在 Generator 函數內部,調用另一個 Generator 函數,默認情況下是沒有效果的,這個就需要用到yield*
表達式,用來在一個 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"
Generator 函數簡單應用
(1)異步操作的同步化表達
Generator
函數的暫停執行的效果,意味着可以把異步操作寫在yield
表達式裏面,等到調用next方法時再往後執行
。這實際上等同於不需要寫回調函數了,因爲異步操作的後續操作可以放在yield表達式下面,反正要等到調用next方法時再執行
。所以,Generator 函數的一個重要實際意義就是用來處理異步操作,改寫回調函數。
function* loadUI() {
showLoadingScreen();
yield loadUIDataAsynchronously();
hideLoadingScreen();
}
var loader = loadUI();
// 加載UI
loader.next()
// 卸載UI
loader.next()
上面代碼中,第一次調用loadUI
函數時,該函數不會執行,僅返回一個遍歷器。下一次對該遍歷器調用next方法,則會顯示Loading
界面(showLoadingScreen)
,並且異步加載數據(loadUIDataAsynchronously)
。等到數據加載完成,再一次使用next
方法,則會隱藏Loading
界面。可以看到,這種寫法的好處是所有Loading
界面的邏輯,都被封裝在一個函數,按部就班非常清晰。
(2)控制流管理
如果有一個多步操作非常耗時,採用回調函數,可能會寫成下面這樣。
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();
上面代碼已經把回調函數,改成了直線執行的形式,但是加入了大量 Promise 的語法。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
}
}