柯里化在異步編程的應用

柯里化

js 中函數做爲一等公民,函數執行中既能夠做爲函數的參數也能夠做爲函數的返回值,而這類執行函數叫作高階函數,利用高階函數的特性很容易就能夠實現柯里化(柯里化(Currying)是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,而且返回接受餘下的參數且返回結果的新函數的技術),根據百科的理解大概就是下面的例子。node

function add(x, y) {
  return x + y
}
function curryind_add(x) {
  return function(y) {
    return x + y
  }
}
const curried_add = curryind_add(1)
curried_add(2) // 3
curried_add(3) // 4

從例子能夠看出,柯里化有防止參數重複的做用,並且具備延遲執行的特徵。下面利用柯里化的特徵看看在異步編程的應用,在開始時先簡單介紹一下 async/await 的原理。編程

Generator 和 promise

async/await 語法實際上是 Generator 和 promise 的語法糖。 promise

Generator 函數執行時會返回一個生成器對象,該對象是一個特殊的迭代器對象,並且自己也是一個可迭代對象(迭代器對象和可迭代對象),下面說說其特殊性。瀏覽器

function *generator() {
  const value = yield 1
  console.log(value)
  try { // 捕獲錯誤
    yield 2
  } catch(e) {
    console.log(e)
  }
}
const iterator = generator();
iterator === iterator[Symbol.iterator](); // true 自身部署了可迭代接口,該接口返回自身
[...iterator] // [1, 2]

const iterator2 = generator(); // 迭代器對象是一次消耗的,須要從新起一個迭代器
iterator2.next() // {value: 1, done: false}
iterator2.next(`value 的值`) // 利用 next 向生成器函數發送數據
iterator2.throw(new Error('error')) // 利用 throw 向生成器函數拋出錯誤

從上面的例子能夠看出,生成器對象,在迭代的過程當中是能夠和生成器函數進行通訊的,若是用在 promise 中,只要把 promise 在狀態改變後執行的回調結果回傳到生成器函數內就能夠實現相似async/await 的效果。而迭代過程,能夠實現一個執行器函數進行迭代,這個函數就相似於執行 async。babel

function createDelayPromise (time) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(time)
    }, time)
  })
} 

function *createIterator() {
  const time1 = yield createDelayPromise(1000)
  console.log(time1)
  const time2 = yield createDelayPromise(2000)
  console.log(time2)
  const time3 = yield createDelayPromise(3000)
  console.log(time3)
  return time1 + time2 + time3
}

function run(createIterator) {
  const iterator = createIterator()
  let result = iterator.next()
  let r  
  const p = new Promise(resolve => {
    r = resolve
  })
  function next() {
    if (result.done) {
      r(result.value)
    } else {
      Promise.resolve(result.value).then(value => {
        result = iterator.next(value)
        next()
      }).catch((err) => {
        result = iterator.throw(err)
        next()
      })
    }
  }
  next()
  return p
}

run(createIterator).then((value) => {
  console.log(value)
})

上面使用 createDelayPromise 封裝了一個 promise 類型的異步任務,而後使用 next 方法去迭代這個迭代器對象。等待對應的異步任務有結果後就經過生成器對象的 next 和 throw 方法把結果回傳到生成器函數中。
上面是使用 promise 配合 generator 可讓異步代碼以相似同步的形式寫在生成器函數中,一樣地對應柯里化後的函數,一樣具備延遲執行的效果,也能夠經過配合 generator 實現相似的效果。app

Generator 和 柯里化

除了一些接口是實現了 promise 化外,有不少比較久的接口依然是使用 callback 類型的接口,像 node 中的不少接口會把 callback 參數放在參數列表的末尾,並把 err 放在回調執行的第一個位置,相似這樣異步

function createDelayCurry (time, callback) {
  setTimeout(() => {
    callback(null, time)
  }, time)
}

下面經過實現不一樣以上的 run 方法,結合柯里化實現以上的效果,此次 yield 後面再也不是 promise 化後的對象,而是柯里化後的函數
在開始以前先實現一個通用的 curry 函數async

const curry = (fn, ...args) => fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args);

這個 curry 函數的做用是等初始存入的函數的參數收集夠就執行,若是不夠就繼續收集,下面會在迭代中補上回調參數,這樣異步任務才能夠執行。函數式編程

const curry = (fn, ...args) => fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args);

function createDelayCurry (time, callback) {
  setTimeout(() => {
    callback(null, time)
  }, time)
} 

const curringDelay = curry(createDelayCurry)

function *createIterator() {
  const time1 = yield curringDelay(1000)
  console.log(time1)
  const time2 = yield curringDelay(2000)
  console.log(time2)
  const time3 = yield curringDelay(3000)
  console.log(time3)
  return time1 + time2 + time3
}

function run(createIterator, callback) {
  const iterator = createIterator()
  let result = iterator.next()
  function next() {
    if (result.done) {
      callback(null, result.value)
    } else {
      result.value((err, value) => { // 補上最後 callback 參數啓動任務
        if (err) {
          result = iterator.throw(err)
          next()
        } else {
          result = iterator.next(value)
          next()
        }
      })
    }
  }
  next()
}

run(createIterator, (err, value) => {
  console.log(err, value)
})

執行效果和 promise 版是差很少同樣的。異步編程

並行

上面是串行的版本,下面看看並行的實現
先看 promise 並行通常寫法

function *createIteratorParallel() {
  const times = yield Promise.all([createDelayPromise(1000), createDelayPromise(2000), createDelayPromise(3000)])
  return times
}

相應的也實現一個 all 方法

function curryAll(curries, callback) {
  let len = curries.length
  let result = []
  let count = 0
  curries.forEach((curried, index) => {
    curried((err, value) => {
      if (err) {
        callback(err)
        return
      } else {
        count++
        result[index] = value
        if (count == len) {
          callback(null, result)
        }
      }
    })
  })
}

const curriedAll = curry(curryAll)

function *createIteratorParallel() {
  const times = yield curriedAll([curringDelay(1000), curringDelay(2000), curringDelay(3000)])
  return times
}

yield 後面依然是柯里化後的函數

es 新語法

上面的方案要求異步任務的回調須要在參數的末尾位置,若是回調不在末尾,那麼就須要修改 curry 函數的實現方式,可是有沒有可能不用修改其實現方式,甚至不用寫 curry 呢?
curry 做爲函數式編程的基本單元,最新的 es 規範實驗性地從語法的角度提供了支持。

function add(x, y) { return x + y; }

const addOne = add(1, ?); // apply from the left
addOne(2); // 3

const addTen = add(?, 10); // apply from the right
addTen(2); // 12

以上的 addOne addTen 就是一個柯里化後的函數,這個語法大部分瀏覽器都沒有支持,若是要使用也很簡單,使用一個 babel 插件就能夠了 babel --plugins @babel/plugin-proposal-partial-application script.js
若是使用了上面的語法,能夠不用引入 curry 函數

function *createIterator() {
  const time1 = yield createDelayCurry(1000, ?)
  console.log(time1)
  const time2 = yield createDelayCurry(2000, ?)
  console.log(time2)
  const time3 = yield createDelayCurry(3000, ?)
  console.log(time3)
  return time1 + time2 + time3
}

若是 callback 在前面,那麼就把問號放在前面,實際上能夠放在任何參數位置上,主要看具體的接口要求。

總結

柯里化的應用很是普遍,上面只是舉了一個簡單的例子,經過這個例子,也瞭解了 async/await 基本實現原理,雖然 async/await 已經被普遍支持,promise 也被普遍使用,可能再也不須要直接使用生成器函數了,但也不妨礙去簡單地瞭解一下。

相關文章
相關標籤/搜索