Generator函數異步應用

轉載請註明出處:html

Generator函數異步應用-掘金node

Generator函數異步應用-博客園git

Generator函數異步應用-知乎es6

上一篇文章詳細的介紹了Generator函數的語法,這篇文章來講一下如何使用Generator函數來實現異步編程。github

或許用Generator函數來實現異步會不多見,由於ECMAScript 2016的async函數對Generator函數的流程控制作了一層封裝,使得異步方案使用更加方便。編程

可是呢,我我的認爲學習async函數以前,有必要了解一下Generator如何實現異步,這樣對於async函數的學習或許能給予一些幫助。數組

文章目錄

  1. 知識點簡單回顧
  2. 異步任務的封裝
  3. thunk函數實現流程控制
  4. Generator函數的自動流程控制
  5. co模塊的自動流程控制

知識點簡單回顧

在Generator函數語法解析篇的文章中有說到,Generator函數能夠定義多個內部狀態,同時也是遍歷器對象生成函數。yield表達式能夠定義多個內部狀態,同時還具備暫停函數執行的功能。調用Generator函數的時候,不會當即執行,而是返回遍歷器對象。promise

遍歷器對象的原型對象上具備next方法,能夠經過next方法恢復函數的執行。每次調用next方法,都會在遇到yield表達式時停下來,再次調用的時候,會在停下的位置繼續執行。調用next方法會返回具備value和done屬性的對象,value屬性表示當前的內部狀態,可能的值有yield表達式後面的值、return語句後面的值和undefined;done屬性表示遍歷是否結束。bash

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異步應用的相關知識也就差很少了,如今稍微總結一下。

  1. 因爲yield表達式能夠暫停執行,next方法能夠恢復執行,這使得Generator函數很適合用來將異步任務同步化。
  2. 可是Generator函數的流程控制會稍顯麻煩,由於每次都須要手動執行next方法來恢復函數執行,而且向next方法傳遞參數以輸出上一個yiled表達式的返回值。
  3. 因而就有了thunk(thunkify)函數和co模塊來實現Generator函數的自動流程控制。
  4. 經過thunk(thunkify)函數分離參數,以閉包的形式將參數逐一傳入,再經過apply或者call方法調用,而後配合使用run函數能夠作到自動流程控制。
  5. 經過co模塊,實際上就是將run函數和thunk(thunkify)函數進行了封裝,而且yield表達式同時支持thunk(thunkify)函數和Promise對象兩種形式,使得自動流程控制更加的方便。

參考資料

  1. Generator 函數的異步應用
  2. node-thunkify
  3. co
相關文章
相關標籤/搜索