轉載請註明出處: "Generator函數非同步應用" 上一篇文章詳細的介紹了Generator函數的語法,這篇文章來說一下如何使用Generator函數來實現非同步編程。 或許用Generator函數來實現非同步會很少見,因為ECMAScript 2016的async函數對Generator函數的流程式控制 ...
轉載請註明出處: Generator函數非同步應用
上一篇文章詳細的介紹了Generator函數的語法,這篇文章來說一下如何使用Generator函數來實現非同步編程。
或許用Generator函數來實現非同步會很少見,因為ECMAScript 2016的async函數對Generator函數的流程式控制製做了一層封裝,使得非同步方案使用更加方便。
但是呢,我個人認為學習async函數之前,有必要瞭解一下Generator如何實現非同步,這樣對於async函數的學習或許能給予一些幫助。
文章目錄
- 知識點簡單回顧
- 非同步任務的封裝
- thunk函數實現流程式控制制
- Generator函數的自動流程式控制制
- co模塊的自動流程式控制制
知識點簡單回顧
在Generator函數語法解析篇的文章中有說到,Generator函數可以定義多個內部狀態,同時也是遍歷器對象生成函數。yield表達式可以定義多個內部狀態,同時還具有暫停函數執行的功能。調用Generator函數的時候,不會立即執行,而是返回遍歷器對象。
遍歷器對象的原型對象上具有next方法,可以通過next方法恢復函數的執行。每次調用next方法,都會在遇到yield表達式時停下來,再次調用的時候,會在停下的位置繼續執行。調用next方法會返回具有value和done屬性的對象,value屬性表示當前的內部狀態,可能的值有yield表達式後面的值、return語句後面的值和undefined;done屬性表示遍歷是否結束。
yield表達式預設是沒有返回值的,或者說,返回值為undefined。因此,想要獲得yield表達式的返回值,就需要給next方法傳遞參數。next方法的參數表示上一個yield表達式的返回值。因此在調用第一個next方法時可以不傳遞參數(即使傳遞參數也不會起作用),此時表示啟動遍歷器對象。所以next方法會比yield表達式的使用要多一次。
更加詳細的語法可以參考這篇文章。傳送門:Generator函數語法解析
非同步任務的封裝
yield表達式可以暫停函數執行,next方法可以恢復函數執行。這使得Generator函數非常適合將非同步任務同步化。接下來會使用setTimeout來模擬非同步任務。
const person = sex => {
return new Promise((resolve, reject) => {
window.setTimeout(() => {
const data = {
sex,
name: 'keith',
height: 180
}
resolve(data)
}, 1000)
})
}
function *gen () {
const data = yield person('boy')
console.log(data)
}
const g = gen()
const next1 = g.next() // {value: Promise, done: false}
next1.value.then(data => {
g.next(data)
})
從上面代碼可以看出,第一次調用next方法時,啟動了遍歷器對象,此時返回了包含value和done屬性的對象,由於value屬性值是promise對象,因此可以使用then方法獲取到resolve傳遞過來的值,再使用帶有data參數的next方法給上一個yield表達式傳遞返回值。
此時在const data = yield person()
這句語句中,就可以得到非同步任務傳遞的參數值了,實現了非同步任務的同步化。
但是上面的代碼會有問題。每次獲取非同步的值時,都要手動執行以下步驟
const g = gen()
const next1 = g.next() {value: Promise, done: false}
next1.value.then(data => {
g.next(data)
})
上面的代碼實質上就是每次都會重覆使用value屬性值和next方法,所以每次使用Generator實現非同步都會涉及到流程式控制制的問題。每次都手動實現流程式控制制會顯得麻煩,有沒有什麼辦法可以實現自動流程式控制制呢?實際上是有的: )
thunk函數實現流程式控制制
thunk函數實際上有些類似於JavaScript函數柯里化,會將某個函數作為參數傳遞到另一個函數中,然後通過閉包的方式為參數(函數)傳遞參數進而實現求值。
函數柯里化實現的過程如下
function curry (fn) {
const args1 = Array.prototype.slice.call(arguments, 1)
return function () {
const args2 = Array.from(arguments)
const arr = args1.concat(args2)
return fn.apply(this, arr)
}
}
使用curry函數來舉一個例子: )
// 需要柯里化的sum函數
const sum = (a, b) => {
return a + b
}
curry(sum, 1)(2) // 3
而thunk函數簡單的實現思路如下:
// ES5實現
const thunk = fn => {
return function () {
const args = Array.from(arguments)
return function (callback) {
args.push(callback)
return fn.apply(this, args)
}
}
}
// ES6實現
const thunk = fn => {
return function (...args) {
return function (callback) {
return fn.call(this, ...args, callback)
}
}
}
從上面thunk函數中,會發現,thunk函數比函數curry化多用了一層閉包來封裝函數作用域。
使用上面的thunk函數,可以生成fs.readFile
的thunk函數。
const fs = require('fs')
const readFileThunk = thunk(fs.readFile)
readFileThunk(fileA)(callback)
使用thunk函數將fs.readFile
包裝成readFileThunk
函數,然後在通過fileA
傳入文件路徑,callback
參數則為fs.readFile
的回調函數。
當然,還有一個thunk函數的升級版本thunkify函數,可以使得回調函數只執行一次。原理和上面的thunk函數非常像,只不過多了一個flag參數用於限制回調函數的執行次數。下麵我對thunkify函數做了一些修改。源碼地址: node-thunkify
const thunkify = fn => {
return function () {
const args = Array.from(arguments)
return function (callback) {
let called = false
// called變數限制callback的執行次數
args.push(function () {
if (called) return
called = true
callback.apply(this, arguments)
})
try {
fn.apply(this, args)
} catch (err) {
callback(err)
}
}
}
}
舉個例子看看: )
function sum (a, b, callback) {
const total = a + b
console.log(total)
console.log(total)
}
// 如果使用thunkify函數
const sumThunkify = thunkify(sum)
sumThunkify(1, 2)(console.log)
// 列印出3
// 如果使用thunk函數
const sumThunk = thunk(sum)
sumThunk(1, 2)(console.log)
// 列印出 3, 3
再來看一個使用setTimeout模擬非同步並且使用thunkify模塊來完成非同步任務同步化的例子。
const person = (sex, fn) => {
window.setTimeout(() => {
const data = {
sex,
name: 'keith',
height: 180
}
fn(data)
}, 1000)
}
const personThunk = thunkify(person)
function *gen () {
const data = yield personThunk('boy')
console.log(data)
}
const g = gen()
const next = g.next()
next.value(data => {
g.next(data)
})
從上面代碼可以看出,value屬性實際上就是thunkify函數的回調函數(也是person的第二個參數),而'boy'則是person的第一個參數。
Generator函數的自動流程式控制制
在上面的代碼中,我們可以將調用遍歷器對象生成函數,返回遍歷器和手動執行next方法以恢復函數執行的過程封裝起來。
const run = gen => {
const g = gen()
const next = data => {
let result = g.next(data)
if (result.done) return result.value
result.value(next)
}
next()
}
使用run函數封裝起來之後,run內部的next函數實際上就是thunk(thunkify)函數的回調函數了。因此,調用run即可實現Generator的自動流程式控制制。
const person = (sex, fn) => {
window.setTimeout(() => {
const data = {
sex,
name: 'keith',
height: 180
}
fn(data)
}, 1000)
}
const personThunk = thunkify(person)
function *gen () {
const data = yield personThunk('boy')
console.log(data)
}
run(gen)
// {sex: 'boy', name: 'keith', height: 180}
有了這個執行器,執行Generator函數就方便多了。不管內部有多少個非同步操作,直接把Generator函數傳入run函數即可。當然,前提是每一個非同步操作,都要是thunk(thunkify)函數。也就是說,跟在yield表達式後面的必須是thunk(thunkify)函數。
const gen = function *gen () {
const f1 = yield personThunk('boy') // 跟在yield表達式後面的非同步行為必須使用thunk(thunkify)函數封裝
const f2 = yield personThunk('boy')
// ...
const fn = yield personThunk('boy')
}
run(gen) // run函數的自動流程式控制制
上面代碼中,函數gen封裝了n個非同步行為,只要執行run函數,這些操作就會自動完成。這樣一來,非同步操作不僅可以寫得像同步操作,而且一行代碼就可以執行。
co模塊的自動流程式控制制
在上面的例子說過,表達式後面的值必須是thunk(thunkify)函數,這樣才能實現Generator函數的自動流程式控制制。thunk函數的實現是基於回調函數的,而co模塊則更進一步,可以相容thunk函數和Promise對象。先來看看co模塊的基本用法
const co = require('co')
const gen = function *gen () {
const f1 = yield person('boy') // 調用person,返回一個promise對象
const f2 = yield person('boy')
}
co(gen) // 將thunk(thunkify)函數和run函數封裝成了co模塊,yield表達式後面可以是thunk(thunkify)函數或者Promise對象
co模塊可以不用編寫Generator函數的執行器,因為它已經封裝好了。將Generator函數co模塊中,函數就會自動執行。
co函數返回一個Promise對象,因此可以用then方法添加回調函數。
co(gen).then(function (){
console.log('Generator 函數執行完成')
})
co模塊原理;co模塊其實就是將兩種自動執行器(thunk(thunkify)函數和Promise對象),包裝成一個模塊。使用co模塊的前提條件是,Generator函數的yield表達式後面,只能是thunk(thunkify)或者Promise對象,如果是數組或對象的成員全部都是promise對象,也可以使用co模塊。
基於Promise對象的自動執行
還是使用上面例子,不過這次是將回調函數改成Promise對象來實現自動流程式控制制。
const person = (sex, fn) => {
return new Promise((resolve, reject) => {
window.setTimeout(() => {
const data = {
name: 'keith',
height: 180
}
resolve(data)
}, 1000)
})
}
function *gen () {
const data = yield person('boy')
console.log(data) // {name: 'keith', height: 180}
}
const g = gen()
g.next().value.then(data => {
g.next(data)
})
手動執行實際上就是層層使用then方法和next方法。根據這個可以寫出自動執行器。
const run = gen => {
const g = gen()
const next = data => {
let result = g.next(data)
if (result.done) return result.value
result.value.then(data => {
next(data)
})
}
next()
}
run(gen) // {name: 'keith', height: 180}
如果對co模塊感興趣的朋友,可以閱讀一下它的源碼。傳送門:co
關於Generator非同步應用的相關知識也就差不多了,現在稍微總結一下。
- 由於yield表達式可以暫停執行,next方法可以恢復執行,這使得Generator函數很適合用來將非同步任務同步化。
- 但是Generator函數的流程式控制制會稍顯麻煩,因為每次都需要手動執行next方法來恢復函數執行,並且向next方法傳遞參數以輸出上一個yiled表達式的返回值。
- 於是就有了thunk(thunkify)函數和co模塊來實現Generator函數的自動流程式控制制。
- 通過thunk(thunkify)函數分離參數,以閉包的形式將參數逐一傳入,再通過apply或者call方法調用,然後配合使用run函數可以做到自動流程式控制制。
- 通過co模塊,實際上就是將run函數和thunk(thunkify)函數進行了封裝,並且yield表達式同時支持thunk(thunkify)函數和Promise對象兩種形式,使得自動流程式控制制更加的方便。
參考資料