【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版)》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章