理解 Generator
Generator 是 ES6 提供的一個新的數(shù)據(jù)類型蟹但,可以叫做 Generator 函數(shù),但跟普通函數(shù)又有些不同拂苹。
其最大特點就是可以交出函數(shù)的執(zhí)行權(quán)(即暫停執(zhí)行)
- 定義時在 function 后面有一個
*
- 可以使用關(guān)鍵字
yield
進(jìn)行多次返回 - 調(diào)用后并不立即執(zhí)行安聘,而是返回一個指向內(nèi)部狀態(tài)的指針對象,該對象是一個遍歷器(Iterator)對象
- 調(diào)用返回遍歷器對象的
next
方法瓢棒,會移動內(nèi)部指針浴韭,使得指針指向下一個狀態(tài)。會返回一個對象脯宿,表示當(dāng)前階段的信息念颈。其中 value 屬性是 yield 語句后面表達(dá)式的值,表示當(dāng)前階段的值连霉;done 屬性表示 Generator 函數(shù)是否執(zhí)行完畢榴芳,即是否還有下一個階段 - 返回遍歷器對象有個
throw
方法可以拋出錯誤,拋出的錯誤可以被函數(shù)體內(nèi)的 try/catch 代碼塊捕獲
備注:yield 只能在 Generator 中使用跺撼,在其他地方使用會報錯
以下用一段代碼作為示例說明
function* genDemo() {
console.log('hello before')
const hello = yield 'hello'
console.log('hello after', hello)
yield 'world'
return 'end'
}
運行結(jié)果如下
- 執(zhí)行 genDemo 這個 Generator 函數(shù)窟感,返回的 g 是一個指針。這個時候沒有任何打印财边,函數(shù)體內(nèi)代碼沒有執(zhí)行
- 執(zhí)行 g.next() 運行到第一個 yield 處停止運行肌括,返回
{ value: 'hello', done: false }
。value 值表示的是 yield 后面的運行結(jié)果酣难,done 表示的是整個 Generator 是否執(zhí)行完畢 - 繼續(xù)執(zhí)行 g.next()谍夭,到第二個 yield 處停止運行,后面還有代碼憨募,所以done 為false
- 繼續(xù)執(zhí)行 g.next(), 執(zhí)行到 return 了紧索,代碼執(zhí)行完畢,所以 done 為 true
- 繼續(xù)執(zhí)行 g.next(), 因為已經(jīng)執(zhí)行完畢了菜谣,繼續(xù)執(zhí)行的話珠漂,都會返回
{ value: undefined, done: true }
注意:以上結(jié)果中晚缩,變量 hello
打印出來的值為 undefined
。說明沒有被賦值媳危。那么該怎么給賦值呢荞彼?
應(yīng)該在執(zhí)行 next 方法的時候傳參來賦值。通過這種方法向 Generator 函數(shù)體內(nèi)輸入數(shù)據(jù)
以下是運行示例
- 第一次執(zhí)行 g.next待笑,運行到第一個 yield 處停止運行鸣皂。這個時候 hello 變量還沒有被賦值
- 第二次執(zhí)行 g.next 時,傳一個參數(shù) ’wmm66’暮蹂,會先將這個傳入的參數(shù)賦值給上一次暫停時 yield 前面要賦值的變量寞缝,即上面代碼中的 hello 變量。賦值完成后再往下運行代碼仰泻,運行到下一個 yield 處
Generator 函數(shù)內(nèi)部還可以部署錯誤處理代碼荆陆,捕獲函數(shù)體外拋出的錯誤。以下是示例代碼和運行結(jié)果
function* genDemo() {
try {
console.log('hello before')
const hello = yield 'hello'
console.log('hello after', hello)
yield 'world'
return 'end'
} catch(err) {
console.warn(err)
}
}
運行 g.throw 拋出錯誤集侯。Generator 函數(shù)內(nèi)部的 try/catch
就可以捕獲到錯誤被啼。發(fā)生錯誤后, Generator 函數(shù)就結(jié)束運行了棠枉,返回 { value: undefined, done: true }
Generator 實現(xiàn)異步編程
相對于異步趟据,我們的思維更容易理解同步代碼。異步編程的語法目標(biāo)是讓代碼變得更像同步代碼
比如以下代碼(這里假設(shè)邏輯上要求讀取完 test1.txt术健,然后再讀取 test2.txt)
const fs = require('fs')
fs.readFile('./test1.txt', function(err, data1) {
if (err) return err
console.log(data1.toString())
fs.readFile('./test2.txt', function(err, data2) {
if (err) return err
console.log(data2.toString())
})
})
該代碼是回調(diào)函數(shù)的方式。這種有先后順序的時候粘衬,會出現(xiàn)多個函數(shù)嵌套荞估,造成閱讀和理解上的障礙。也就是常說的“回調(diào)地獄”
我們把它改成 promise
的寫法
const fs = require('fs')
const promiseReadFile = (file) => {
return new Promise((resolve, reject) => {
fs.readFile(file, function(err, data) {
if (err) return reject(err)
resolve(data)
})
})
}
promiseReadFile('./test1.txt')
.then(data1 => {
console.log(data1.toString())
})
.then(() => {
return promiseReadFile('./test2.txt')
})
.then(data2 => {
console.log(data2.toString())
}).catch(err => {
console.error(err)
})
這種鏈?zhǔn)秸{(diào)用的方式稚新,邏輯上清晰了不少
我們計劃使用 Generator
函數(shù)勘伺,通過如下編碼實現(xiàn)異步
const genReadFile = function* (){
const data1 = yield readFile('./test1.txt')
console.log(data1.toString())
const data2 = yield readFile('./test2.txt')
console.log(data2.toString())
}
Generator
函數(shù)和 yield
本身跟異步?jīng)]有關(guān)系。yield 是對函數(shù)的執(zhí)行流程進(jìn)行變更褂删,是控制函數(shù)執(zhí)行流程用的飞醉,而恰好這個控制流程的機制能夠簡化回調(diào)函數(shù)和 promise 的調(diào)用
我們現(xiàn)在要做的就是寫一個方法,用來自動控制 Generator 函數(shù)的流程屯阀,接收和交還程序的執(zhí)行權(quán)
在這之前缅帘,先了解一下 thunk 函數(shù)
thunk函數(shù)介紹
最早的 thunk 函數(shù)起源于 “傳值調(diào)用” 和 “傳名調(diào)用” 之爭
let x = 1
function fn(m) {
return m * 2
}
fn(x + 1)
- 傳值調(diào)用的主張:執(zhí)行前就進(jìn)行計算。先計算 x + 1 = 2难衰,然后將值2傳入fn 方法中:
m * 2
- 傳名調(diào)用的主張:只在執(zhí)行的時候才進(jìn)行計算钦无。將 x + 1 直接傳入到fn方法中:
(x + 1) * 2
Javascript 是 “傳值調(diào)用”。在 JavaScript 語言中盖袭,Thunk 函數(shù)替換的不是表達(dá)式失暂,而是多參數(shù)函數(shù)彼宠,將其替換成單參數(shù)的版本,且只接受回調(diào)函數(shù)作為參數(shù)
// 正常版本的readFile
const fn = fs.readFile(file, callback)
// thunk版本的readFile
function Thunk(file) {
return function(callback) {
return fn(file, callback)
}
}
const thunkReadFile = Thunk(file)
thunkReadFile(callback)
該代碼中的 thunkReadFile 函數(shù)弟塞,只接受回調(diào)函數(shù)作為參數(shù)凭峡。這個單參數(shù)版本,就叫做 Thunk
函數(shù)
任何函數(shù)只要存在回調(diào)就可以 thunk 轉(zhuǎn)換决记,下面是一個簡單的 thunk 轉(zhuǎn)換器
const Thunk = function(fn) {
return function() {
const args = Array.prototype.slice.call(arguments)
return function(callback) {
args.push(callback)
return fn.apply(this, args)
}
}
}
使用上面的轉(zhuǎn)換器摧冀,生成 fs.readFile 的 Thunk 函數(shù)。
const readFileThunk = Thunk(fs.readFile)
readFileThunk(file)(callback)
有個插件 thunkify
可以直接轉(zhuǎn)換
const fs = require('fs')
const thunkify = require('thunkify')
const readFile = thunkify(fs.readFile)
readFile('./test1.txt')(function(err, str) {
// ...
})
實現(xiàn)回調(diào)函數(shù)的異步
Thunk 函數(shù)現(xiàn)在可以用于 Generator 函數(shù)的自動流程管理霉涨。我們讓 Generator 函數(shù)的 yield 后面都執(zhí)行 Thunk 函數(shù)按价。就改造出了我們想要的代碼
const fs = require('fs')
const thunkify = require('thunkify')
const readFile = thunkify(fs.readFile)
const genReadFile = function* (){
const data1 = yield readFile('./test1.txt')
console.log(data1.toString())
const data2 = yield readFile('./test2.txt')
console.log(data2.toString())
}
那么應(yīng)該怎么執(zhí)行呢?我們先來分析一下
- 第一次執(zhí)行 next 方法笙瑟,會返回對象中的 value 是一個函數(shù)楼镐,該函數(shù)可以傳入一個callback,這個 callback 里面能夠獲取到
./test1.txt
文件讀出來的數(shù)據(jù) - 讀出來
./test1.txt
文件的數(shù)據(jù)后往枷,我們需要繼續(xù)執(zhí)行 next框产,此時需要傳入從./test1.txt
文件讀出來的數(shù)據(jù),這樣才能將該數(shù)據(jù)賦值給 Generator 函數(shù)中的 data1 變量错洁。 - 然后同上...
所以執(zhí)行方法如下
const g = genReadFile()
const r1 = g.next()
r1.value(function(err, data1){
if (err) throw err
const r2 = g.next(data1)
r2.value(function(err, data2){
if (err) throw err
g.next(data2)
})
})
我們寫一個 Generator 自動執(zhí)行器秉宿,用來執(zhí)行 Generator 函數(shù)
function run(gen) {
const g = gen()
function next(err, data) {
const result = g.next(data)
if (result.done) return
result.value(next)
}
next()
}
run(genReadFile)
實現(xiàn) Promise 的異步
思路與回調(diào)函數(shù)相同,具體代碼如下
const fs = require('fs')
const readFile = (file) = >{
return new Promise((resolve, reject) = >{
fs.readFile(file, function(err, data) {
if (err) return reject(err)
resolve(data)
})
})
}
const genReadFile = function * () {
const data1 = yield readFile('./test1.txt')
console.log(data1.toString())
const data2 = yield readFile('./test2.txt')
console.log(data2.toString())
}
手動執(zhí)行代碼如下
const g = genReadFile()
g.next().value.then(function(data1) {
g.next(data1).value.then(function(data2) {
g.next(data2)
})
})
Generator 自動執(zhí)行器代碼如下
function run(gen) {
const g = gen()
function next(data) {
const { value, done } = g.next(data)
if (done) return value
value.then(function(data) {
next(data)
})
}
next()
}
run(genReadFile)
co函數(shù)
有個成熟的函數(shù)庫 co
屯碴。該函數(shù)庫接受 Generator 函數(shù)作為參數(shù)描睦,返回一個 Promise 對象
Thunk
函數(shù)示例
const fs = require('fs')
const thunkify = require('thunkify')
const co = require('co')
const readFile = thunkify(fs.readFile)
const genReadFile = function * () {
const data1 = yield readFile('./test1.txt')
console.log(data1.toString())
const data2 = yield readFile('./test2.txt')
console.log(data2.toString())
}
co(genReadFile)
Promise
對象示例
const fs = require('fs')
const co = require('co')
const readFile = (file) = >{
return new Promise((resolve, reject) = >{
fs.readFile(file, function(err, data) {
if (err) return reject(err)
resolve(data)
})
})
}
const genReadFile = function * () {
const data1 = yield readFile('./test1.txt')
console.log(data1.toString())
const data2 = yield readFile('./test2.txt')
console.log(data2.toString())
}
co(genReadFile)
Async/await 對比
ES7 引入了 async 函數(shù)。一句話导而,async 函數(shù)就是 Generator 函數(shù)的語法糖
Generator 函數(shù)封裝的異步代碼示例
const genReadFile = function* () {
const data1 = yield readFile('./test1.txt')
console.log(data1.toString())
const data2 = yield readFile('./test2.txt')
console.log(data2.toString())
}
co(genReadFile)
改寫成 async 函數(shù)寫法
const asyncReadFile = async function() {
const data1 = await readFile('./test1.txt')
console.log(data1.toString())
const data2 = await readFile('./test2.txt')
console.log(data2.toString())
}
asyncReadFile()
以上兩段代碼對比忱叭,async 函數(shù)就是將 Generator 函數(shù)的星號 *
替換成 async
,放在 function 關(guān)鍵字前面今艺,將 yield
替換成 await
韵丑。
async 函數(shù)對 Generator 函數(shù)的改進(jìn),體現(xiàn)在以下幾點
- 內(nèi)置執(zhí)行器:Generator 函數(shù)的執(zhí)行必須靠執(zhí)行器(以上代碼就是使用 co 模塊執(zhí)行)虚缎,而 async 函數(shù)自帶執(zhí)行器撵彻。async 函數(shù)的執(zhí)行,與普通函數(shù)一模一樣
- 更好的語義:async 表示函數(shù)里有異步操作实牡,await 表示緊跟在后面的表達(dá)式需要等待結(jié)果陌僵。語義更清晰
- 更廣的適用性:co 模塊約定,yield 命令后面只能是 Thunk 函數(shù)或 Promise 對象创坞,而 async 函數(shù)的 await 命令后面拾弃,可以是 Promise 對象和原始類型的值
- 返回值是 Promise:async 函數(shù)的返回值是 Promise 對象,這比 Generator 函數(shù)的返回值是 Iterator 對象方便多了摆霉『来唬可以用 then 方法指定下一步的操作