ES6 Generator 基礎指南

本文翻譯自:The Basics Of ES6 Generatorsjavascript

因爲我的能力有限,翻譯中不免有紕漏和錯誤,望不吝指正issuejava

JavaScript ES6(譯者注:ECMAScript 2015)中最使人興奮的特性之一莫過於Generator函數,它是一種全新的函數類型。它的名字有些奇怪,初見其功能時甚至更會有些陌生。本篇文章旨在解釋其基本工做原理,並幫助你理解爲何Generator將在將來JS中發揮強大做用。node

Generator從運行到完成的工做方式

但咱們談論Generator函數時,咱們首先應該注意到的是,從「運行到完成」其和普通的函數表現有什麼不一樣之處。git

不論你是否已經意識到,你已經潛意識得認爲函數具備一些很是基礎的特性:函數一旦開始執行,那麼在其結束以前,不會執行其餘JavaScript代碼。es6

例如:github

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()函數執行的過程當中,咱們帶有console.log(...)的定時器並不可以中斷foo()函數的運行。所以代碼被阻塞,定時器被推入事件循環的最後,耐心等待foo函數執行完成。編程

假若foo()能夠被中斷執行?它不會給咱們的帶來史無前例的浩劫嗎?設計模式

函數能夠被中斷對於多線程編程來講確實是一個挑戰,可是值得慶幸的是,在JavaScript的世界中咱們不必爲此而擔憂,由於JS老是單線程的(在任什麼時候間只有一條命令/函數被執行)。數組

注意: Web Workers是JavaScript中實現與JS主線程分離的獨立線程機制,總的說來,Web Workers是與JS主線程平行的另一個線程。在這兒咱們並不介紹多線程併發的一個緣由是,主線程和Web Workers線程只可以經過異步事件進行通訊,所以每一個線程內部從運行到結束依然遵循一個接一個的事件循環機制。瀏覽器

運行-中止-運行

因爲ES6Generators的到來,咱們擁有了另一種類型的函數,這種函數能夠在執行的過程當中暫停一次或屢次,在未來的某個時間繼續執行,而且容許在Generator函數暫停的過程當中運行其餘代碼。

若是你曾經閱讀過關於併發或者多線程編程的資料,那你必定熟悉「協程」這一律念,「協程」的意思就是一個進程(就是一個函數)其能夠自行選擇終止運行,以即可以和其餘代碼「協做」完成一些功能。這一律念和「preemptive」相對,preemptive認爲能夠在進程/函數外部對其終止運行。

根據ES6 Generator函數的併發行爲,咱們能夠認爲其是一種「協程」。在Generator函數體內部,你可使用yield關鍵字在函數內部暫停函數的執行,在Generator函數外部是沒法暫停一個Generator函數執行的;每當Generator函數遇到一個yield關鍵字就將暫停執行。

而後,一旦一個Generator函數經過yield暫停執行,其不可以自行恢復執行,須要經過外部的控制來從新啓動generator函數,咱們將在文章後面部分介紹這是怎麼發生的。

基本上,只要你願意,一個Generator函數能夠暫停執行/從新啓動任意屢次。實際上,你能夠再Generator函數內部使用無限循環(好比非著名的while (true) { .. })來使得函數能夠無盡的暫停/從新啓動。而後這在普通的JS程序中倒是瘋狂的行徑,甚至會拋出錯誤。可是Generator函數卻可以表現的很是明智,有些時候你確實想利用Generator函數這種無盡機制。

更爲重要的是,暫停/從新啓動不只僅用於控制Generator函數執行,它也能夠在generator函數內部和外部進行雙向的通訊。在普通的JavaScript函數中,你能夠經過傳參的形式將數據傳入函數內容,在函數內部經過return語句將函數的返回值傳遞到函數外部。在generator函數中,咱們經過yield表達式將信息傳遞到外部,而後經過每次重啓generator函數將其餘信息傳遞給generator。

Generator 函數的語法

然咱們看看新奇而且使人興奮的generator函數的語法是怎樣書寫的。

首先,新的函數聲明語法:

function *foo() {
    // ..
}

發現*符號沒?顯得有些陌生且有些奇怪。對於從其餘語言轉向JavaScript的人來講,它看起來很像函數返回值指針。可是不要被迷惑到了,*只是用於標識generator函數而已。

你可能會在其餘的文章/文檔中看到以下形式書寫generator函數function* foo(){},而不是這樣function *foo() {}(*號的位置有所不一樣)。其實兩種形式都是合法的,可是最近我認爲後面一種形式更爲準確,所以在本篇文章中都是使用後面一種形式。

如今,讓咱們來討論下generator函數的內部構成吧。在不少方面,generator函數和普通函數無異,只有在generator函數內部有一些新的語法。

正如上面已經說起,咱們最早須要瞭解的就是yield關鍵字,yield__被視爲「yield表達式」(並非一條語句),由於當咱們從新啓動generator函數的時候,咱們能夠傳遞信息到generator函數內部,不論咱們傳遞什麼進去,都將被視爲yield__表達式的運行結果。

例如:

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

yield "foo"表達式會在generator函數暫停時把「foo」字符串傳遞到外部。同時,當generator函數恢復執行的時候,其餘的值又會經過其餘表達式傳入到函數裏面做爲yield表達式的返回值加1最後再將結果賦值給x變量。

看到generator函數的雙向通訊了嗎?generator函數將‘’foo‘’字符串傳遞到外部,暫停函數執行,在未來的某個時間點(多是當即也多是很長一段時間後),generator會被重啓,而且會傳遞一個值給generator函數,就好像yield關鍵字就是某種發送請求獲取值的請求形式。

在任意表達式中,你能夠僅使用yield關鍵字,後面不跟任何表達式或值。在這種狀況下,就至關於將undefined經過yield傳遞出去。以下代碼:

// note: `foo(..)` here is NOT a generator!!
function foo(x) {
    console.log("x: " + x);
}

function *bar() {
    yield; // just pause
    foo( yield ); // pause waiting for a parameter to pass into `foo(..)`
}

Generator 迭代器

「Generator 迭代器」,是否是至關晦澀難懂?

迭代器是一種特殊的行爲,準確說是一種設計模式,當咱們經過調用next()方法去遍歷一組值的集合時,例如,咱們經過在長度爲5的數組[1, 2, 3, 4, 5]上面實現了迭代器。當咱們第一次調用next()的時候,會返回1。第二次調用next()返回2,如此下去,當全部的值都返回後,再次調用next()將返回null或者false或其餘值,這意味着你已經遍歷完真個數組中的值了。

咱們是經過和generator迭代器進行交互來在generator函數外部控制generator函數,這聽起來比起實際上有些複雜,考慮下面這個愚蠢的(簡單的)例子:

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

爲了遍歷*foo()generator函數中的全部值,咱們首先須要構建一個迭代器,咱們怎麼去構建這個迭代器呢?很是簡單!

var it = foo();

如此之簡單,咱們僅僅想執行普通函數同樣執行generator函數,其將返回一個迭代器,可是generator函數中的代碼並不會運行。

這彷佛有些奇怪,而且增長了你的理解難度。你甚至會停下來思考,問爲何不經過var it = new foo()的形式來執行generator函數呢,這語法後面的緣由可能至關複雜並超出了咱們的討論範疇。

好的,如今讓咱們開始迭代咱們的generator函數,以下:

var message = it.next();

經過上面的語句,yield表達式將1返回到函數外部,可是返回的值可能比想象中會多一些。

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

在每一調用next()後,咱們實際上從yield表達式的返回值中獲取到了一個對象,這個對象中有value字段,就是yield返回的值,同時還有一個布爾類型的done字段,其用來表示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 }

有趣的是,當咱們獲取到值爲5的時候,done字段依然是false。這由於,實際上generator函數還麼有執行徹底,咱們還能夠再次調用next()。若是咱們向函數內部傳遞一個值,其將被設置爲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函數的最終返回值也許並非一個最佳實踐,由於當咱們經過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 );

// note: not sending anything into `next()` here
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 }

你能夠看到,咱們依然能夠經過foo(5)傳遞參數(在例子中是x)給generator函數,就像普通函數同樣,是的參數x5.

在第一次執行next(..)的時候,咱們並無傳遞任何值,爲何?由於在generator內部並無yield表達式來接收咱們傳遞的值。

假如咱們真的在第一次調用next(..)的時候傳遞了值進去,也不會帶來什麼壞處,它只是將這個傳入的值拋棄而已。ES6代表,generator函數在這種狀況只是忽略了這些沒有被用到的值。(注意:在寫這篇文章的時候,Chrome和FF的每夜版支持這一特性,可是其餘瀏覽有可能沒有徹底支持這一特性甚至可能會拋出錯誤)(譯者注:文章發佈於2014年)

yield(x + 1)表達式將傳遞值6到外部,在第二次調用next(12)時候,傳遞12到generator函數內部做爲yield(x + 1)表達式的值,所以y被賦值爲12 * 2,值爲24。接下來,下一條yield(y / 3)(yield (24 / 3))將向外傳遞值8。第三次調用next(13)傳遞13到generator函數內部,給yield(y / 3)。是的z被設置爲13.

最後,return (x + y + z)就是return (5 + 24 + 13),也就是42將會做爲最終的值返回出去。

從新閱讀幾遍上面的實例。最開始有些難以理解。

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 ); // still `5`, not `6` :(

正如你所見,經過調用foo()生成的迭代器經過for..of循環來迭代,循環自動幫你對迭代器進行遍歷迭代,每次迭代返回一個值,直到done: true,只要done: false,每次循環都將從value屬性上獲取到值賦值給迭代的變量(例子中的v)。一旦當donetrue。循環迭代結束。(for..of循環不會對generator函數最終的return值進行處理)

正如你所看到的,for..of循環忽略了generator最後的return 6的值,同時,循環沒有暴露next()出來,所以咱們也不可以向generator函數內傳遞數據。

總結

OK,上面是關於generator函數的基本用法,若是你依然對generator函數感到費解,不要擔憂,咱們全部人在一開始感受都是那樣的。

咱們很天然的想到這一外來的語法對咱們實際代碼有什麼做用呢?generator函數有不少做用,咱們只是挖掘了其很是粗淺的一部分。在咱們發現generator函數如此強大以前咱們應該更加深刻的瞭解它。

在你練習上面代碼片斷以後(在Chrome或者FF每夜版本,或者0.11+帶有--harmony的node環境下),下面的問題也許會浮出水面:(譯者注:現代瀏覽器最新版本都已支持Generator函數)

  1. 怎樣處理generator內部錯誤?

  2. 在generator函數內部怎麼調用其餘generator函數?

  3. 異步代碼怎麼和generator函數協同工做?

這些問題,或者其餘的問題都將在隨後的文章中覆蓋,敬請期待。

相關文章
相關標籤/搜索