理解 Generator (生成器)

理解 Generator (生成器)

generator 和 iterator 的概念非常類似,同樣都是調 next() 獲取當前元素和當前狀態,同樣都是惰性求值,同樣可以用 for...of 遍歷。

按我的理解,generator 就是一種 iterator,只不過 generator 是由函數轉換成的 itarator,這種轉換是 es6 內部提供的,而將一個函數轉爲 generator,只需藉助 function*yield

舉例來說:

const gen = function* () {
    yield 'hello'
    yield 'world'
}
const gener = gen() // 得到一個生成器,或叫遍歷器
gener.next() // { value: 'hello', done: false }
gener.next() // { value: 'world', done: false }
gener.next() // { value: undefined, done: true}

理解了 generator 就是 function 變出來的 itarator,就可以參考 iterator 了。不過 genertor 也有與 iterator 不一樣的地方:

在 next 中回傳參數

const gen = function* () {
    console.log('1st: ' + (yield 'hello'))
    console.log('2nd: ' + (yield 'world'))
}
const gener = gen()

console.log(gener.next('first next'))
console.log(gener.next('second next'))
console.log(gener.next('third next'))

打印結果爲:

{ value: 'hello', done: false }
1st: second next
{ value: 'world', done: false }
2nd: third next
{ value: undefined, done: true }

next 方法可以帶一個參數,可以當作上一條 yield 語句的返回值。

generator 可以用 return 提前結束遍歷

const gen = function* () {
    yield 'hello'
    yield 'world'
}
const gener = gen()
gener.next() // { value: 'hello', done: false }
gener.return('ended') // { value: 'ended, done: true }
gener.next() // { value: undefined, done: true }

可在遍歷器外部調用 throw 拋出錯誤,而在遍歷器內部捕獲

const gen = function* () {
    try {
        yield 'hello'
        yield 'world'
    } catch (e) {
        console.log(e)
    }
}

const gener = gen()
gener.next()
gener.throw('unexpected')

generator 嵌套

當 generator 嵌套時,內部 generator 需要 yield* generator

const foo = function* () {
    yield 'hello'
    yield 'world'
}

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

const gener = bar()

for (let itm of gener) {
    console.log(itm)  // 'bar', 'hello', 'world'
}

generator - 變異步爲 ‘同步’

generator 與 curry 的結合可以使異步應用用同步的方式書寫,使得代碼更爲清晰簡潔。

curry (柯里化)

curry 是以數學家 Haskell Curry 的名字命名的。Haskell Curry 也是 Haskell 語言的發明者。

curry 是一種 函數參數 拆分技術。通過拆分函數參數,我們可以得到新的函數。使用 curry 以後,函數變得更加靈活,更具有可玩性。

下面以 add 函數舉例說明 curry 的用處:

function add () {
    const args = Array.prototype.slice.apply(arguments)
    return args.reduce((a, b) => a + b)
}
const add1 = add.curry(1)
const add123 = add.curry(1, 2, 3)
console.log(add1(1, 2, 3))   // 7
console.log(add123(4, 5, 6)) // 21

add1 其實是將 1 與傳遞給 add1 的參數組合在一起,再傳遞給 add 函數。

add123 其實是將 1, 2, 3 與傳遞給 add123 的參數組合在一起,再傳遞給 add 函數。

下面給出 curry 技術的 js 實現:

Function.method('curry', function () {
    const slice = Array.prototype.slice,
        args = slice.apply(arguments),
        that = this
    return function () {
        return that.apply(null, args.concat(slice.apply(arguments)))
    }
})

上面的代碼有點繞,主要是 arguments 是一個具有 length 屬性的 Array like,不得不先用 slice做點轉換。使用 es6 可以簡化:

Function.method('curry', function (...args) {
    const that = this
    return (...args2) => that.apply(null, args.concat(args2))
})

generator 中使用 curry

參考了阮一峯老師的《Generator 函数的异步应用

 使用 promise 逐一讀取文件

fs.readFile 是一個異步函數,讀取一個文件的用法:

const fs = require('fs')
fs.readFile('./file.js', (err, data) => {
    if (err) throw err
    else console.log(data.toString())
})

當有多個文件,需要逐一讀取時,如果用 promise 可以這樣做:

const fs = require('fs')

const read = function (fileName) {
    return new Promise((resolve, reject) => {
        fs.readFile(fileName, (err, data) => {
            if (err) reject(err)
            else resolve(data)
        })
    })
}
read('./file1.js')
    .then(data => console.log(data.toString())) // 處理 file1.js
    .then(() => read('./file2.js'))
    .then(data => console.log(data.toString())) // 處理 file2.js
    .catch(err => console.log(err))

console.log('started...')

執行結果爲:

started...
// file1 內容
// file2 內容

雖然 .then 的寫法避開了 回調黑洞,使代碼的寫法近似同步,但過多的 then 還不夠簡潔優雅。

使用 generator 逐一讀取文件

在使用 generator 時,我們可以用 next(value) 往生成器中回傳參數。當初並不明白 next(value) 有何意義,如今可以解惑了。看下面這個函數:

function* readGen () {
    const r1 = yield read('./file1.js') // 暫且不管 read 函數的定義
    console.log(r1.toString())
    const r2 = yield read('./file2.js')
    console.log(r2.toString())
}
const g = readGen()
g.next()

我希望 readGen 中的 file1.jsfile2.js 能依次讀取。

要實現這個目標,我得利用 g.next() 獲取 generator 執行狀態,並且處理完一個文件後,再次調用 g.next() 進行下一步處理。直到 done === 'true'

g.next() 的狀態中,value 可以用來放 readFile 的回調函數。而這,正式 curry 大顯身手的地方。

下面來實現 read 函數:

const read = function (fileName) {
    return fs.readFile.curry(fileName) // 返回只含一個 callback 參數的函數
}

這樣就將 readFile 變成了只有一個 callback 參數的函數。當執行 g.next() 時,這個函數賦予了 value,我們就能在 g.next().value(cb) 中處理了。

注意 read 函數,它的功能是返回了一個函數,這個函數記錄了 fileName。這也可以用閉包實現:

const read = function (fileName) {
    return function (cb) {
        return fs.readFile(fileName, cb)
    }
}

關鍵 之處在於,我們需要在 value 中獲取到 readFile 的回調信息,並且能夠繼續 g.next() 使函數繼續執行。下面的 run 函數確保 readGen 逐一執行直到完成:

function run (gen) {
    const g = gen()

    function next (err, data) {
        if (data) console.log(data.toString())
        const ret = g.next(data)
        if (ret.done) return
        else ret.value(next)
    }

    next()
}

next 函數就是往 curry 化之後那個函數裏傳的參數。通過在 next 中判斷 generator 是否執行完成(如果還沒執行完,則繼續調用 g.next() ),最終實現文件的逐一讀取。

總結一下,使用 generator 將異步變爲同步,就是

  • 利用 function*yield 以同步的方式書寫代碼
  • 定義一個自動執行器,使 generator 執行完成。

從 generator 到 sync

由於 generator 實現了以同步方式書寫代碼,並在實際使用中反饋良好,但畢竟需要自己實現執行方式,於是大家就想爲什麼不提供一種新的語法來做這件事呢,它能包含隱式的自動執行方式,使代碼更爲簡潔優雅。

這個語法就是 syncawait 。注意它還不是 es6 的標準,而是屬於 es7 ,但也可以用 es6 實現。在不支持 es7 的環境中,可以用 babel 轉碼。我在 node v8.9.0 中測試已支持 aync

sync 函數的使用十分簡單,只相當於對上面的 generator 的方式作了兩個修改:

  • *sync
  • yield → await

於是,採用 syncawait 改寫上面的代碼:

const fs = require('fs')
const readAll = async function () {
    const r1 = await fs.readFile('./file1.js')
    console.log(r1.toString())
    const r2 = await fs.readFile('./file2.js')
    console.log(r2.toString())
}
readAll()
console.log('--- begin ---')

執行結果:

--- begin ---
// content of file1.js
// content of file2.js

採用 async 時,需注意:

  • await 後面可以是 Promise 或者 原始類型值,如果是 promise,因爲可能 reject,最好放在 try…catch 裏。
  • async 返回一個 Promise,可用 then 添加下一步操作。

結語

這是我學習 generator 過程中的總結,主要參考了《javascript 語言精粹》和 《ES6 標準入門》。这两本书都非常不錯,可以作为手边书,隨時翻閱。

generator 的用法很好理解,但深入以後才發現並不簡單,其中牽扯了許多知識點,比如 IteratorcurryPromiseasync,還包含了人們爲解決異步書寫問題所作出的很多努力。

以上內容僅反映我對一些概念的當下理解,並不完善,比如 “generator 與 協程” 就沒提及。同時也深感要向讀者解釋清楚一個概念需要多少努力,除非自己能深入理解並舉一反三,而且要邏輯清晰,並用儘可能簡單的語言闡釋出來,否則就可能誤人子弟,浪費別人時間了。推薦購買正版《ES6 標準入門》,遇到不懂的好隨時查閱。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值