一文掌握生成器 Generator筹误,利用 Generator 實現(xiàn)異步編程

理解 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é)果如下

圖一.png
  1. 執(zhí)行 genDemo 這個 Generator 函數(shù)窟感,返回的 g 是一個指針。這個時候沒有任何打印财边,函數(shù)體內(nèi)代碼沒有執(zhí)行
  2. 執(zhí)行 g.next() 運行到第一個 yield 處停止運行肌括,返回 { value: 'hello', done: false }。value 值表示的是 yield 后面的運行結(jié)果酣难,done 表示的是整個 Generator 是否執(zhí)行完畢
  3. 繼續(xù)執(zhí)行 g.next()谍夭,到第二個 yield 處停止運行,后面還有代碼憨募,所以done 為false
  4. 繼續(xù)執(zhí)行 g.next(), 執(zhí)行到 return 了紧索,代碼執(zhí)行完畢,所以 done 為 true
  5. 繼續(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ù)

以下是運行示例

二.png
  1. 第一次執(zhí)行 g.next待笑,運行到第一個 yield 處停止運行鸣皂。這個時候 hello 變量還沒有被賦值
  2. 第二次執(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í)行呢?我們先來分析一下

  1. 第一次執(zhí)行 next 方法笙瑟,會返回對象中的 value 是一個函數(shù)楼镐,該函數(shù)可以傳入一個callback,這個 callback 里面能夠獲取到 ./test1.txt 文件讀出來的數(shù)據(jù)
  2. 讀出來 ./test1.txt 文件的數(shù)據(jù)后往枷,我們需要繼續(xù)執(zhí)行 next框产,此時需要傳入從 ./test1.txt 文件讀出來的數(shù)據(jù),這樣才能將該數(shù)據(jù)賦值給 Generator 函數(shù)中的 data1 變量错洁。
  3. 然后同上...

所以執(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)在以下幾點

  1. 內(nèi)置執(zhí)行器:Generator 函數(shù)的執(zhí)行必須靠執(zhí)行器(以上代碼就是使用 co 模塊執(zhí)行)虚缎,而 async 函數(shù)自帶執(zhí)行器撵彻。async 函數(shù)的執(zhí)行,與普通函數(shù)一模一樣
  2. 更好的語義:async 表示函數(shù)里有異步操作实牡,await 表示緊跟在后面的表達(dá)式需要等待結(jié)果陌僵。語義更清晰
  3. 更廣的適用性:co 模塊約定,yield 命令后面只能是 Thunk 函數(shù)或 Promise 對象创坞,而 async 函數(shù)的 await 命令后面拾弃,可以是 Promise 對象和原始類型的值
  4. 返回值是 Promise:async 函數(shù)的返回值是 Promise 對象,這比 Generator 函數(shù)的返回值是 Iterator 對象方便多了摆霉『来唬可以用 then 方法指定下一步的操作
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末奔坟,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子搭盾,更是在濱河造成了極大的恐慌咳秉,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鸯隅,死亡現(xiàn)場離奇詭異澜建,居然都是意外死亡,警方通過查閱死者的電腦和手機蝌以,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進(jìn)店門炕舵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人跟畅,你說我怎么就攤上這事咽筋。” “怎么了徊件?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵奸攻,是天一觀的道長。 經(jīng)常有香客問我虱痕,道長睹耐,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任部翘,我火速辦了婚禮硝训,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘新思。我一直安慰自己捎迫,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布表牢。 她就那樣靜靜地躺著,像睡著了一般贝次。 火紅的嫁衣襯著肌膚如雪崔兴。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天蛔翅,我揣著相機與錄音敲茄,去河邊找鬼。 笑死山析,一個胖子當(dāng)著我的面吹牛堰燎,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播笋轨,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼秆剪,長吁一口氣:“原來是場噩夢啊……” “哼赊淑!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起仅讽,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤陶缺,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后洁灵,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體饱岸,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年徽千,在試婚紗的時候發(fā)現(xiàn)自己被綠了苫费。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡双抽,死狀恐怖百框,靈堂內(nèi)的尸體忽然破棺而出绷耍,到底是詐尸還是另有隱情誓焦,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布晒杈,位于F島的核電站柑贞,受9級特大地震影響方椎,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜钧嘶,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一棠众、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧有决,春花似錦闸拿、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至台汇,卻和暖如春苛骨,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背苟呐。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工痒芝, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人牵素。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓严衬,卻偏偏與公主長得像,于是被迫代替她去往敵國和親笆呆。 傳聞我的和親對象是個殘疾皇子请琳,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容