[譯] 關(guān)于 ES6 生成器 Generator 的探索

原文鏈接:https://medium.freecodecamp.org/lets-explore-es6-generators-5e58ed23b0f1
作者:Tiago Lopes Ferreia
譯者:Soyaine

Generator

生成器(Generators)是可迭代協(xié)議(iterables)的一種實現(xiàn)。

它的最大特點在于,它是可以在保持上下文狀態(tài)的情況下暫停執(zhí)行的函數(shù),當(dāng)處理需要暫停的執(zhí)行邏輯,但在恢復(fù)執(zhí)行時需要之前的上下文時,這就起到了很重要的作用??吹竭@里有沒有想到了異步編程?

語法

生成器的語法以 function* 作為開頭的標(biāo)識符(注意末尾的星號),同時穿插著 yield 來實現(xiàn)執(zhí)行的暫停。下面以具體的代碼演示這個過程。

function* generator() {
    // 代碼A
    yield 'foo'
    // 代碼B
}

調(diào)用上面的 generator 函數(shù)時,將會創(chuàng)建一個新的生成器,我們可以通過 next 函數(shù)來用它控制程序邏輯。運行 next 將會執(zhí)行這個生成器中代碼,直到 yield 的語句。到這個點時,yield 語句中的 value 的值將被返回,同時 generator 的執(zhí)行也被掛起。

const g = generator()

g.next() // { value: 'foo', done: false }
// 生成器中的代碼A 被執(zhí)行,
// value 中的 'foo' 通過 yield 被返回,
// 然后,生成器的執(zhí)行被掛起。

g.next() // { value: undefined, done: fales }
// 在這個階段,剩下的代碼(如代碼B)繼續(xù)被執(zhí)行,
// 由于沒有返回值,所以我得到 'undefined' 作為 value 的值,
// 同時這個迭代器(iterator)返回 true 以表示迭代結(jié)束。

yield

yield 伴隨著生成器出現(xiàn),并且允許我們在執(zhí)行的中途返回值,但只能在生成器內(nèi)部這樣做。如果我們試圖在回調(diào)時返回一個值的話,就算在生成器中聲明了,也會報錯。

function* generator() {
    ['foo', 'bar'].forEach(e => yield e) // SyntaxError 語法錯誤
    // 我們不能在一個非 generator 的函數(shù)中使用 yield
    //(譯者注:此例子從 yield 被寫在了 forEach 函數(shù)中)
}

yield*

yield* 是用來在一個生成器中調(diào)用另一個生成器的。

function* foo() {
    yield 'foo'
}

// 我們?nèi)绾卧?bar 生成器中調(diào)用 foo 生成器呢?
function* bar() {
    yield 'bar'
    foo()
    yield 'bar again'
}

const b = bar();

b.next() // { value: 'bar', done: false }
b.next() // { value: 'bar again', done: false }
b.next() // { value: undefined, done: true }

這里的 b 迭代器由 bar 生成器產(chǎn)生,但在調(diào)用 ‘foo’ 的時候并不會按照期待執(zhí)行。這是因為,盡管運行 ‘foo’ 時產(chǎn)生了一個迭代器,但我們并不會迭代它。這就是 ES6 會帶來 yield* 操作符的原因。

function* foo() {
    yield 'foo'
}

function* bar() {
    yield 'bar'
    yield* foo()
    yield 'bar again'
}

const b = bar();

b.next() // { value: 'bar', done: false }
b.next() // { value: 'foo', done: false }
b.next() // { value: 'bar again', done: false }
b.next() // { value: undefined, done: true }

這對數(shù)據(jù)迭代也完美適用:

function* foo() {
    yield 'foo'
}

function* bar() {
    yield 'bar'
    yield* foo()
    yield 'bar again'
}

for (let e of bar()) {
    console.log(e)
    // bar
    // foo
    // bar again
}

console.log([...bar()]) // [ 'bar', 'foo', 'bar again' ]

yield* 中遍歷這個生成器中的所有元素,然后 yield 返回這個值。

function* bar() {
    yield 'bar'
    for (let e of foo()) {
        yield e
    }
    yield 'bar again'
}

作為迭代器(Iterators)的生成器(Generators)

Generators as Iterators

之所以說生成器是簡單的迭代器,是因為它們同樣遵循了可迭代協(xié)議(iterable protocol)和迭代器協(xié)議(iterator protocol):

  • 可迭代協(xié)議指出,一個可迭代的對象應(yīng)該返回一個含有 key 為 Symbol.iterator 的迭代器。
    const g = generator()
    
    typeof g[Symbol.iterator] // function
    
  • 迭代器協(xié)議指出,迭代器對象應(yīng)該是一個指向迭代函數(shù)(iteration)中下一個元素的對象,并且這個對象包含一個叫 next 的函數(shù)。
    const iterator = g[Symbol.iterator]()
    typeof iterator.next // function
    
  • 由于生成器是可迭代對象,所以我們可以使用遍歷方法來迭代循環(huán)其中的值,例如 for-of。
    for (let e of iterator) {
         console.log(e)
         // 'foo'
    }
    

Return

我們可以在生成器中添加一個 return 語句,但由于生成器的數(shù)據(jù)處理規(guī)則是迭代,所以 return 的作用與平常有所區(qū)別。

function* generatorWithReturn() {
    yield 'foo'
    yield 'bar'
    return 'done'
}

var g = generatorWithReturn()

g.next() // { value: 'foo', done: false }
g.next() // { value: 'bar', done: false }
g.next() // { value: 'done', done: true }

當(dāng)手動執(zhí)行這個迭代函數(shù)的時候,使用 next,我們得到的迭代器的最后一個 valuereturn 的返回值(如 done),此時 done 的標(biāo)記值為 true。

但是在使用一個像 for-of 或者 destructuring 這樣定義好的數(shù)據(jù)遍歷操作時,返回值會被忽略。

for (let e of g) {
     console.log(e)
     // 'foo'
     // 'bar'
}

console.log([...g]) // ['foo', 'bar']

yield*

我們知道 yield* 允許我們在一個生成器中調(diào)用另一個生成器,同時它也允許我們通過已執(zhí)行的生成器來存儲返回值。

function* foo() {
     yield 'foo'
     return 'foo done'
}

function* bar() {
     yield 'bar'
     const result = yield* foo()
     yield result
}

for (let e of bar()) {
     console.log(e)
     // bar
     // foo
     // foo done
}

Throw

我們可以在生成器內(nèi)部使用 throw,next 會傳遞發(fā)生的異常。一旦拋出異常,迭代器的流程將會中斷,狀態(tài)被設(shè)置為 done: true,并無限保持。

function* generatorWithThrow() {
     yield 'foo'
     throw new Error('Ups!')
     yield 'bar'
}

var g = generatorWithReturn()

g.next() // { value: 'foo', dxone: false }
g.next() // Error: Ups!
g.next() // { value: undefined, done: true }

用于數(shù)據(jù)遍歷的生成器

生成器除了因 yield 而有了數(shù)據(jù)生成的作用,使用 next 它也可以遍歷數(shù)據(jù)。

function* generatorDataConsumer() {
     // A
     console.log('Ready to consume!')
     while (true) {
          const input = yield; // B
          console.log(`Got: ${input}`)
     }
}

這里也有一些有趣的點可以探索。(請看代碼及之后的解釋)

// (1)
var g = generatorDataConsumer()

// (2)
g.next() // { value: undefined, done: false }
// Ready to consume!

// (3)
g.next('foo') // { value: undefined, done: false }
// Got: foo

創(chuàng)建生成器 (1)

在這一步創(chuàng)建了一個 g 生成器,程序會在 A 處停止執(zhí)行。

第一個 next (2)

執(zhí)行第一個 next 時,生成器會執(zhí)行到第一個 yield 語句。第一次執(zhí)行的時候,通過 next 傳遞的任何值都會被忽略,這是因為沒有出現(xiàn) yield 語句。所以執(zhí)行過程會在 B 處暫停,等待 yield 被賦值。

下一個 next (3)

在下一次執(zhí)行 next 時,生成器會運行代碼至下一個 yield。在我們上面的例子中,它打印了通過 yield 得到的值(i.e. Got: foo),并且再次在 yield 處暫停。

應(yīng)用例子

Use Cases

實現(xiàn)可迭代協(xié)議

因為生成器是可迭代協(xié)議的一種實現(xiàn),所以當(dāng)它被創(chuàng)建的時候我們也就得到了一個可迭代的對象,在這個對象中,yield 代表了每次迭代時傳遞出的值。這使得我們可以用生成器來創(chuàng)建迭代器。

下面這個例子展示了一個可迭代的生成器遍歷數(shù)據(jù)并找出最大值的過程。因為我們的生成器會返回一個可迭代對象,所以我們可以用 for-of 來遍歷這些值。

記住 yield 會暫停生成器的執(zhí)行過程,每次迭代時,生成器會從之前暫停的地方重新開始。

function* evenNumbersUntil() {
    for (let value = 0; value <= max; value += 2) {
        // 當(dāng) 'value' 的值是偶數(shù)時,我們把它 'yield'
        // 并作為迭代過程中的下一個值
        if (value % 2 === 0) yield value;
    }
}

// 現(xiàn)在我們可以用 'for-of' 來迭代這些值了
for (let e of evenNumbersUntil(10)) {
    console.log(e)
    // 0
    // 2
    // 4
    // 6
    // 8
    // 10
}

異步的代碼

我們可以用生成器來更好實現(xiàn)異步代碼,比如 promises。這部分的例子很好的介紹了 ES8 中的新特性 async/await

下面要展示一個用 promises 來獲取 JSON 文件的例子,我們將用 Jake Archibald 的在 developers.google.com 上的例子。

為了讓我們的代碼看起來更像同步的風(fēng)格,我們使用了 co 庫。至于 async/await 的話,我們的代碼會有點像我們之前的版本。

結(jié)尾

這是 Axel Rauschmayer 在 Exploring ES6 上的圖,向我們展示了生成器是如何與迭代器聯(lián)系在一起的。

Exploring ES6

生成器是可迭代協(xié)議的一種實現(xiàn),且遵循可迭代協(xié)議和迭代器協(xié)議,因此它們都可以用于建立可迭代對象。

生成器最令人驚奇的點在于它可以在執(zhí)行過程中暫停,為此 ES6 引入了 yield 的新特性。

然而在生成器中調(diào)用生成器并非像執(zhí)行生成器函數(shù)那么簡單,為了解決這個問題,ES6 中又有了 yield*

生成器為讓異步編程向同步風(fēng)格的發(fā)展更近了一步。

致謝

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容