【ES6】Generator函數詳解
引言:從Generator開始,纔算是ES6相對高級的部分。之後的Promise、async都與異步編程有關。 |
一、Generator函數簡介
先用最直白的話給大家介紹一下Generator函數:首先呢,Generator是一類函數,通過 * 號來定義。其次,Generator函數裏特有的yield關鍵字,可以把函數裏面的語句在執行時分步執行。用next()來執行。
例如,定義一函數:
function* test(){
yield console.log(“1”);
yield console.log(“2”);
yield console.log(“3”);
}
var t=test();t.next();t.next();t.next();
在執行第一個next的時候,輸出1,第二個輸出2,以此類推……這樣,就把函數裏面的內容分段執行了。
下面請看詳細介紹。
基本概念
Generator函數是ES6提供的一種異步編程解決方案,語法行爲與傳統函數完全不同。對於Generator函數有多種理解角度。從語法上,首先可以把它理解成一個狀態自動機,封裝了多個內部狀態。
執行Generator函數會返回一個遍歷器對象。也就是說,Generator函數除了是狀態機,還是一個遍歷器對象生成函數。返回的遍歷器對象,可以依次遍歷Generator函數內部的每一個狀態。
形式上,Generator 函數是一一個普通函數,但是有兩個特徵:一是function命令與函數名之間有一個星號;二是函數體內使用yield語句定義不同的內部狀態。
/******** 代碼塊1-1 ********/
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
代碼塊1-1定義了一個Generator函數helloWorldGenerator, 它內部有兩個yield語句"hello"和“world",即該函數有3個狀態: hello、 world 和return語句(結束執行)。Generator函數的調用方法與普通函數一樣,也是在函數名後面加上一對圓括號。不同的是,調用Generator函數後,該函數並不執行,返回的也不是函數運行結果,而是一個指向內部狀態的指針對象。
下一步,必須調用遍歷器對象的next方法,使得指針移向下一個狀態。也就是說,每次調用next方法,內部指針就從函數頭部或上一次停下來的地方開始執行,直到遇到下一條yield語句(或return語句)爲止。換言之,Generator 函數是分段執行的,yield語句是暫停執行的標記,而next方法可以恢復執行。
/******** 代碼塊1-2 ********/
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
上面的代碼塊1-2共調用了4次next方法。運行解釋如下:第1次調用,Generator 函數開始執行,直到遇到第一條yield語句爲止。next方法返回一個對象,它的value屬性就是當前yield語句的值hello,done屬性的值false表示遍歷還沒有結束。
第2次調用,Generator 函數從上次yield語句停下的地方,一直執行到下一條yield語句。
第3次調用,Generator 函數從上次yield語句停下的地方,一直執行到return語句(如果沒有return語句,就執行到函數結束)。next方法返回的對象的value屬性就是緊跟在return語句後面的表達式的值(如果沒有return語句,則value屬性的值爲undefined),done屬性的值true表示遍歷已經結束。
第4次調用,此時Generator函數已經運行完畢,next方法返回的對象的value屬性爲undefined, done屬性爲true。以後再調用next方法,返回的都是這個值。
總結:調用Generator函數,返回一個遍歷器對象,代表Generator函數的內部指針。以後,每次調用遍歷器對象的next方法,就會返回一個有着value和done兩個屬性的對象。value屬性表示當前的內部狀態的值,是yield語句後面那個表達式的值; done 屬性是一個布爾值,表示是否遍歷結束。
函數寫法
ES6沒有規定functon關鍵字與函數名之間的星號寫在哪個位置。這導致下面代碼塊1-3的寫法都能通過。/******** 代碼塊1-3 ********/
function * foo(x, y) { ... }
function *foo(x, y) { ... }
function* foo(x, y) { ... }
function*foo(x, y) { ... }
yield關鍵字介紹
由於Generator函數返回的遍歷器對象只有調用next方法纔會遍歷下一個內部狀態,所以其實提供了一種可以暫停執行的函數。yield語句就是暫停標誌。遍歷器對象的next方法的運行邏輯如下。
1、遇到yield語句就暫停執行後面的操作。並將緊跟在yield後的表達式的值作爲返回的對象的value屬性值。
2、下一次調用next方法時再繼續往下執行,直到遇到下條yield語句。
3、如果沒有再遇到新的yield語句,就一直運行到函數結束,直到returnr語句爲止,並將return語句後面的表達式的值作爲返回的對象的value屬性值。
4、如果該函數沒有return語句,則返回的對象的value屬性值爲undefined。
另外注意,yield語句不能用在普通函數中,否則會報錯。
二、next方法的參數
yield語句本身沒有返回值,或者說總是返回undefined。next方法可以帶一個參數, 該參數會被當作上一條yield語句的返回值。/******** 代碼塊2-1 ********/
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:false}
var b = foo(5);
b.next() // {value:6,done:false }
b.next(12) // {value:8, done:false }
b.next(13) // {value:42, done:true }
代碼塊2-1中,第二次運行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函數,且此時不再需要調用next方法。如代碼塊3-1。
/******** 代碼塊3-1 ********/
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循環中。
四、關於普通throw()與Generator的throw()
Generator函數返回的遍歷器對象都有一個throw方法,可以在函數體外拋出錯誤,然後在Generator函數體內捕獲。我們知道在try...catch語句中,如果try語句中拋出了兩個異常,當第一個異常拋出時,就會直接停止。
/******** 代碼塊4-1 ********/
var g = function* () {
while (true) {
try {
yield;
} catch (e) {
if (e != 'a') throw e;
console.log('內部捕獲', e);
}
}
};
var i = g();
i.next();
try {
i.throw('a');
i.throw('b');
} catch (e) {
console.log('外部捕獲',e);
}
//內部捕獲a
//外部捕獲b
但是,上面的代碼塊4-1中,遍歷器對象i連續拋出兩個錯誤。第一個錯誤被Generator函數體內的catch語句捕獲,然後Generator函數執行完成,於是第二個錯誤被函數體外的catch語句捕獲。注意,不要混淆遍歷器對象的throw方法和全局的throw命令。上面的錯誤是用遍歷器對象的throw方法拋出的,而不是用throw命令拋出的。後者只能被函數體外的catch語句捕獲。
五、Generator函數的應用【很重要】
1、延遲函數
功能:對函數f()延遲2000ms後執行。見代碼塊5-1。/******** 代碼塊5-1 ********/
function * f(){
console.log('執行了');
}
var g = f();
setTimeout(function () {
g.next();
},2000);
2、簡化函數的flag(Generator與狀態機)
功能:把一些需要flag的函數,去掉flag,大大簡化函數體。見代碼塊5-2與5-3。/******** 代碼塊5-2 原函數 ********/
var tickFlag = true;
var clock = function (){
if(tickFlag)
console.log('Tick');
else
console.log('Tock');
tickFlag=!tickFlag;
}
/******** 代碼塊5-3 簡化後函數 ********/
var clock = function* (){
while(true){
yield console.log('Tick');
yield console.log('Tock');
}
}
3、異步操作的同步化表達
功能:假如現在有兩個api分別是加載頁面和卸載頁面。普通寫法見代碼塊5-4,同步化表達見代碼塊與5-5。/******** 代碼塊5-4 原寫法 ********/
//加載頁面
showLoadingScreen();
//加載頁面數據
loadUIDataAsynchronously();
//卸載頁面
hideLoadingScreen();
/******** 代碼塊5-5 同步化後寫法 ********/
function* loadUI(){
showLoadingScreen();
yield loadUIDataAsynchronously();
hideLoadingScreen();
}
var load = loadUI();
//加載UI
load.next();
//卸載UI
load.next();
其實,類似代碼塊5-5的寫法,Vue裏面有個概念Bus(中央總線),還有Java裏面的線程的總線,都極爲相似。感興趣可以去查一查。
4、函數的自動化控制【心生佩服】
功能:如果有一個多步操作非常耗時,採用回調函數可能會很複雜。這時利用Generator函數可以改善代碼運行的流程,類似於自動化控制。見代碼塊5-6。/******** 代碼塊5-6 函數的自動化控制 ********/
function* longRunningTask() {
try {
var value1 = yield step1();
var value2 = yield step2(value1);
var value3 = yield step3(value2);
var value4 = yield step4(value3);
} catch (e) {
// catch Error
}
}
scheduler(longRunningTask());//實現自動化控制
function scheduler(task){
setTimeout(function() {
var taskObj = task.next(task.value);
if(!taskObj.done){
task.value = taskObj.value;
}
},0);
}
查看更多ES6教學文章:
1. 【ES6】let與const 詳解
2. 【ES6】變量的解構賦值
3. 【ES6】字符串的拓展
4. 【ES6】正則表達式的拓展
5. 【ES6】數值的拓展
6. 【ES6】數組的拓展
7. 【ES6】函數的拓展
8. 【ES6】對象的拓展
9. 【ES6】JS第7種數據類型:Symbol
10. 【ES6】Proxy對象
11. 【ES6】JS的Set和Map數據結構
12. 【ES6】Generator函數詳解
13. 【ES6】Promise對象詳解
14. 【ES6】異步操作和async函數
15. 【ES6】JS類的用法class
16. 【ES6】Module模塊詳解
17. 【ES6】ES6編程規範 編程風格
參考文獻
阮一峯 《ES6標準入門(第2版)》