ES6 Generators基本概念

  ES6 Generators系列:

  1. ES6 Generators基本概念
  2. 深刻研究ES6 Generators
  3. ES6 Generators的異步應用
  4. ES6 Generators併發

  在JavaScript ES6提供的諸多使人興奮的新特性中,有一個新函數類型,叫generator。名字聽起來很怪(咱們姑且將它稱之爲生成器函數),並且行爲更加讓人以爲怪異。本文旨在解釋generator函數的一些基本知識,用來講明它是如何工做的,並幫助你瞭解爲何它會讓將來的JS變得如此強大。html

 

運行-完成(Run-To-Completion)

  首先咱們要討論的是generator函數和普通函數在運行方式上有什麼區別。node

  不論你是否已經意識到了,對於函數而言,你老是會假定一個原則:一旦函數開始運行,它就會在其它JS代碼運行以前運行到結束。這句話怎麼理解呢?看下面的代碼:編程

setTimeout(function(){
    console.log("Hello World");
},1);

function foo() {
    // NOTE: don't ever do crazy long-running loops like this
    for (var i=0; i<=1E10; i++) {
        console.log(i);
    }
}

foo();
// 0..1E10
// "Hello World"

  這裏的for循環須要一個比較長的時間來執行完,顯然超過1毫秒。在foo()函數運行過程當中,上面的setTimeout函數不會被運行直到foo()函數運行結束。設計模式

  那若是事情不是這樣的會怎麼樣?若是foo()函數的運行會被setTimeout打斷呢?是否是咱們的程序將會變得不穩定?數組

  在多線程運行的程序中,這的確會給你帶來噩夢,好在JavaScript是單線程運行的(同一時間只有一條命令或函數會被運行),所以這一點你沒必要擔憂。瀏覽器

  注意,Web開發容許JS程序的一部分在一個獨立的線程裏運行,該線程能夠與JS主線程並行運行。但這並不意味着咱們能夠在JS程序中引入多線程操做,由於在多線程操做中兩個獨立的線程之間是能夠經過異步事件相互通訊的,它們彼此之間經過事件輪詢機制(event-loop)一次一個地來運行。多線程

 

運行-中止-運行(Run-Stop-Run)

  ES6的generator函數容許在運行的過程當中暫停一次或屢次,隨後再恢復運行。暫停的過程當中容許其它的代碼執行。併發

  若是你曾經讀過有關併發或者線程編程方面的文章,你也許見到過"cooperative"(協做)一詞,它說明了一個進程(這裏能夠將它理解爲一個function)自己能夠選擇什麼時候被中斷以便與其它代碼進行協做。這個概念與"preemptive"(搶佔式。進程調度的一種方式。當前進程在運行過程當中,若是有重要或緊迫的進程到達(其狀態必須爲就緒),則該進程將被迫放棄處理機,系統將處理機馬上分配給新到達的進程。)正好相反,它代表了一個進程或function能夠被其自身的意願打斷。異步

  在ES6中,generator函數使用的都是cooperative類型的併發方式。在generator函數體內,經過使用新的yield關鍵字從內部將函數的運行打斷。除了generator函數內部的yield關鍵字,你不可能從任何地方(包括函數外部)中斷函數的運行。異步編程

  不過,一旦generator函數被中斷,它不可能自行恢復運行,除非經過外部的控制來從新啓動這個generator函數。稍後我會介紹如何實現這一點。

  基本上,按照須要,一個generator函數在運行中能夠被中止和從新啓動屢次。事實上,你徹底能夠指定一個無限循環的generator函數(就像while(true){...}語句同樣),它永遠也不會被執行完。不過在一個正常的JS程序中,咱們一般不會這樣作,除非代碼寫錯了。Generator函數足夠理性,有時候它偏偏就是你想要的!

  而更重要的是,這種中止和啓動不只僅控制着generator函數的執行,它還容許信息的雙向傳遞。普通函數在開始的時候獲取參數,在結束的時候return一個值,而generator函數能夠在每次yield的時候返回值,而且在下一次從新啓動的時候再傳入值。

 

語法

  是時候介紹一下generator函數的語法了:

function *foo() {
    // ..
}

  注意這裏的*了嗎?這是一個新引入的運算符,對於學習C語言系的同窗而言,可能會想到函數指針。不過這裏千萬不要把它和指針的概念混淆了,*運算符在這裏只是用來標識generator函數的類型。

  你可能在其它的文章或文檔中看到這種寫法function* foo(){},而本文中咱們使用這種寫法function *foo(){}(區別僅僅是*的位置)。這兩種寫法都是正確的,不過咱們推薦使用後者。

  咱們來看看generator函數的內容。Generator函數在大多數方面就是普通的JS函數,所以咱們須要學習的新語法不會不少。

  在generator函數體內部主要是yield關鍵字的應用,前面咱們已經提到過它。注意這裏的yield ___被稱之爲yield表達式而不是語句,這是由於當咱們從新啓動generator函數時,咱們會傳入一個值,而無論這個值是什麼,都會做爲yield ___表達式計算的結果。

  一個例子:

function *foo() {
    var x = 1 + (yield "foo");
    console.log(x);
}

  這裏的yield "foo"表達式會在generator函數暫停時返回字符串"foo",當下一次generator函數從新啓動時,無論傳入的值是什麼,都會做爲yield表達式計算的結果。這裏會將表達式1 + 傳入值的結果賦值給變量x

  從這個意義上來講,generator函數具備雙向通訊的功能。Generator函數暫停的時候返回了字符串"foo",稍後(多是當即,也多是從如今開始一段很長的時間)從新啓動的時候它會請求一個新值並將最終計算的結果返回。這裏的yield關鍵字起到了請求新值的做用。

  在任何表達式中,你能夠只用yield關鍵字而不帶其它內容,此時yield返回的值是undefined。看下面的例子:

// 注意,這裏的函數foo(..)不是一個generator函數!!
function foo(x) {
    console.log("x: " + x);
}

function *bar() {
    yield; // 暫停執行,返回值是undefined
    foo( yield ); // 暫停執行,稍後將獲取到的值做爲函數foo(..)的參數傳入
}

 

Generator遍歷器

  「Generator遍歷器」!乍一看,好像很難懂!

  遍歷器是一種特殊的行爲,其實是一種設計模式,咱們經過調用next()方法來遍歷一組有序的值。想象一下,例如使用遍歷器對數組[1,2,3,4,5]進行遍歷。第一次調用next()方法返回1,第二次調用next()方法返回2,以此類推。當數組中的全部值都返回後,調用next()方法將返回nullfalse或其它可能的值用來表示數組中的全部元素都已遍歷完畢。

  咱們惟一能夠從外部控制generator函數的方式就是構造和經過遍歷器進行遍歷。這聽起來好像有點複雜,考慮下面這個簡單的例子:

function *foo() {
    yield 1;
    yield 2;
    yield 3;
    yield 4;
    yield 5;
}

  爲了遍歷generator函數*foo(),首先咱們須要構造一個遍歷器。怎麼作?很簡單!

var it = foo();

  事實上,經過普通的方式調用一個generator函數並不會真正地執行它。

  這有點讓人難以理解。你可能在想,爲何不是var it = new foo(). 背後的原理已經超出了咱們的範圍,這裏咱們不展開討論。

  而後,咱們經過下面的方法對generator函數進行遍歷:

var message = it.next();

  這會執行yield 1表達式並返回值1,但不只限於此。

console.log(message); // { value:1, done:false }

  事實上每次調用next()方法都會返回一個object對象,其中的value屬性就是yield表達式返回的值,而屬性done是一個boolean類型,用來表示對generator函數的遍歷是否已經結束。

  繼續看剩餘的幾個遍歷:

console.log( it.next() ); // { value:2, done:false }
console.log( it.next() ); // { value:3, done:false }
console.log( it.next() ); // { value:4, done:false }
console.log( it.next() ); // { value:5, done:false }

  有趣的是,當value的值是5done仍然是false。這是由於從技術上來講,generator函數尚未執行完,咱們必須再調用一次next()方法,若是此時傳入一個值(若是未傳入值,則默認爲undefined),它會被設置爲yield 5表達式計算的結果,而後generator函數纔算執行完畢。

  所以:

console.log( it.next() ); // { value:undefined, done:true }

  因此,最終的結果是咱們完成了generator函數的調用,可是最後一次的遍歷並無返回任何值,這是由於全部的yield表達式都已經被執行完了。

  你或許在想,咱們能夠在generator函數中使用return語句嗎?若是能夠的話,那value屬性的值會被返回嗎?

答案是確定的:

function *foo() {
    yield 1;
    return 2;
}

var it = foo();

console.log( it.next() ); // { value:1, done:false }
console.log( it.next() ); // { value:2, done:true }

可是:

  依賴generator函數中return語句返回的值並不值得提倡,由於當使用for..of循環(下面會介紹)來遍歷generator函數時,最後的return語句可能會致使異常。

  咱們來完整地看一下在遍歷generator函數時信息是如何被傳入和傳出的:

function *foo(x) {
    var y = 2 * (yield (x + 1));
    var z = yield (y / 3);
    return (x + y + z);
}

var it = foo( 5 );

// 注意這裏在調用next()方法時沒有傳入任何值
console.log( it.next() );       // { value:6, done:false }
console.log( it.next( 12 ) );   // { value:8, done:false }
console.log( it.next( 13 ) );   // { value:42, done:true }

  你能夠看到咱們在構造generator函數遍歷器的時候仍然能夠傳遞參數,這和普通的函數調用同樣,經過語句foo(5),咱們將參數x的值設置爲5。

  第一次調用next()方法時,沒有傳入任何值。爲何呢?由於此時沒有yield表達式來接收咱們傳入的值。

  若是在第一次調用next()方法時傳入一個值,也不會有任何影響,該值會被拋棄掉。按照ES6標準的規定,此時generator函數會直接忽略掉該值(注意:在撰寫本文時,Chrome和FireFox瀏覽器都能很好地符合該規定,但其它瀏覽器可能並不徹底符合,並且可能會拋出異常)。

  表達式yield(x + 1)的返回值是6,而後第二個next(12)12做爲參數傳入,用來代替表達式yield(x + 1),所以變量y的值就是12 × 2,即24。隨後的yield(y / 3)(即yield(24 / 3))返回值8。而後第三個next(13)13做爲參數傳入,用來代替表達式yield(y / 3),因此變量z的值是13

  最後,語句return (x + y + z)return (5 + 24 + 13),因此最終的返回值是42

  多重溫幾回上面的代碼,開始的時候你會以爲很難懂,只要理解了generator函數執行的過程,掌握起來並不難。

 

for..of循環

  ES6還從語法層面上對遍歷器提供了直接的支持,即for..of循環。看下面的例子:

function *foo() {
    yield 1;
    yield 2;
    yield 3;
    yield 4;
    yield 5;
    return 6;
}

for (var v of foo()) {
    console.log( v );
}
// 1 2 3 4 5

console.log( v ); // 仍然是5,而不是6

  正如你所看到的,由foo()建立的遍歷器被for..of循環自動捕獲,而後自動進行遍歷,每遍歷一次就返回一個值,直到屬性done的值爲true。只要屬性done的值爲false,它就會自動提取value屬性的值並將其傳遞給迭代變量(本例中爲變量v)。一旦屬性done的值爲true,循環遍歷就中止(並且不會包含函數的返回值,若是有的話。因此此處的return 6不包括在for..of循環中)。

  如上所述,能夠看到for..of循環忽略並拋棄了返回值6,這是由於此處沒有對應的next()方法被調用,for..of循環不支持將值傳遞給generator函數迭代的狀況,如在for..of循環中使用next(v)。事實上,在使用for..of循環時不須要使用next方法。

 

總結

  以上就是generator函數的基本概念。若是你仍然以爲有點難以理解,也不用太擔憂,任何人剛開始接觸generator函數時都會有這種感受!

  你應該會很天然地想到generator函數能在本身的代碼中起到什麼樣的做用,儘管咱們會在不少地方用到它。咱們剛剛只是接觸到了一些皮毛,還有不少須要瞭解的,因此咱們必須深刻研究,才能發現它是如此的強大。

  嘗試在Chrome nightly/canary或FireFox nightly或node 0.11+(使用--harmony參數)環境中運行本文的示例代碼,並思考下面的問題:

  1. 如何處理異常?
  2. 在一個generator函數中能夠調用另外一個generator函數嗎?
  3. 如何在generator函數中進行異步編程?

  接下來的文章會解答上述問題,並繼續深刻探討有關ES6 generator函數的內容,敬請關注!

相關文章
相關標籤/搜索