譯/用Generators解決callback金字塔

譯 用Generators解決callback金字塔

what is generator

Generators算得上js的一個新概念函數。它看起來像是一個函數,可是能夠暫停執行。從語法上看,有3點關注:html

  1. 函數名前有一個*
  2. 函數內有yield
  3. 調用時返回一個對象,對這個對象調用next纔會繼續執行

你的node支持generator了嗎?

在node 0.11以上,對node必須加入--harmony 參數,便可支持:前端

$ node --harmony
    > function *a(){}
    undefined
    >

看到undefined就說明支持了。若是不加參數,默認不支持。你會看到node

$ node
    > function *a(){}
    ...

Generators in ES6

聲明一個generator 是這樣的:git

function* ticketGenerator() {}

若是想要 generator 提供一個值並暫停,那麼須要使用yeild 關鍵字。yield 就像 return 同樣返回一個值。和它不一樣的是,yield會暫停函數。es6

function* ticketGenerator() {
      yield 1;
      yield 2;
      yield 3;
    }

咱們作了一個迭代器,叫作 ticketGenerator. 咱們能夠和它要一個值,而後它返回1,而後暫停。依次返回2,3:github

var takeANumber = ticketGenerator();
    takeANumber.next(); 
    // > { value: 1, done: false }
    takeANumber.next(); 
    // > { value: 2, done: false }
    takeANumber.next(); 
    // > { value: 3, done: false }
    takeANumber.next(); 
    // > { value: undefined, done: true }

如今咱們返回最大爲3 ,不太有用.咱們能夠遞增到無窮。無窮數列來了。web

function* ticketGenerator() {
  for(var i=0; true; i++) {
    yield i;
  }
}

再來一遍:npm

var takeANumber = ticketGenerator();
console.log(takeANumber.next().value); //0
console.log(takeANumber.next().value); //1
console.log(takeANumber.next().value); //2
console.log(takeANumber.next().value); //3
console.log(takeANumber.next().value); //4

每次疊加,無窮枚舉。這就有點意思了。app

干預yield返回值

除了能夠枚舉累加外, next() 還有第二種用法:若是你傳遞一個值給next,它會做爲yield 語句的返回值。咱們能夠利用這個特性,把剛剛的無限數列作一次重置:框架

function* ticketGenerator() {
  for(var i=0; true; i++) {
    var reset = yield i;
    if(reset) { i = -1; }
  }
}

這樣當咱們調用next(true)的時候,i會等於-1,從而重設了i值到初始值。

無需困惑, yield i 會把i發到generator的調用next處,可是generator內部咱們使用next提供的值(若是提供了的話)。

看看效果:

var takeANumber = ticketGenerator();
console.log(takeANumber.next().value); //0
console.log(takeANumber.next().value); //1
console.log(takeANumber.next().value); //2
console.log(takeANumber.next(true).value); //0
console.log(takeANumber.next().value); //1

這就是generator。古怪,有趣。接下來會繼續分析它的應用,在解決callback hell方面。

問題

拿一個案例開刀吧。咱們來看一個delay函數,延遲一些時間,而後打印點文字:

function delay(time, callback) {
  setTimeout(function () {
    callback("Slept for "+time);
  }, time);
}

調用它也是常見代碼:

delay(1000, function(msg) {
  console.log(msg);
  delay(1200, function (msg) {
      console.log(msg);
    }
})
//...waits 1000ms
// > "Slept for 1000"
//...waits another 1200ms
// > "Slept for 1200"

爲了保證兩次打印的次序,咱們惟一的辦法,就是經過callback來完成。

要是延遲多幾回,好比12次,咱們須要12次嵌套的callback,代碼不段向右延伸。哇,回調金字塔。

呼叫Generators

異步是node的靈魂。但是異步的麻煩在於callback,由於我餓每一年須要等待完成通知,因此須要callback。

有了 generators,咱們可讓代碼等。無需callback。能夠用generators 在每一個異步調用進行中,在調用next()以前暫停執行。還記得yield/next 嗎?這是generators的絕活。

怎麼弄?

咱們首先得知道,咱們要把異步調用暫停須要用到generator ,而不是典型的函數,因此加個星在函數前:

function* myDelayedMessages() {
    /* delay 1000 ms and print the result */
    /* delay 1200 ms and print the result */
}

加入delay調用。delay須要callback。這個callback須要調用generator.next(),以便繼續代碼。咱們先放一個空的callback:

function* myDelayedMessages() {
    console.log(delay(1000, function(){}));
    console.log(delay(1200, function(){}));
}

如今代碼依然是異步的。加入yield:

function* myDelayedMessages() {
    console.log(yield delay(1000, function(){}));
    console.log(yield delay(1200, function(){}));
}

又近了一步。可是須要有人告訴generator向前走,走起來。

關鍵概念在這裏:當delay完成時,須要在它的callback內執行點東西,這些東西讓generator向前走(調用next)

這個函數,且無論如何實現,咱們知道它得叫作resume:

function* myDelayedMessages(resume) {
    console.log(yield delay(1000, resume));
    console.log(yield delay(1200, resume));
}

咱們得把定義好的resume傳遞給 myDelayedMessages

變魔術了...

如何實現resume,它又如何知道咱們的generator?

給generator 函數加個外套,外套函數的功能就是啓動generator,傳遞寫好的resume,第一次撥動generator(調用next),等待resume被調用,在resume內繼續撥動generator。這樣generator就能夠滾動起來了:

function run(generatorFunction) {
    var generatorItr = generatorFunction(resume);
    function resume(callbackValue) {
        generatorItr.next(callbackValue);
    }
    generatorItr.next()
}

有點燒腦。當年寫做名聞遐邇的「goto statement considered harmful」的做者,看到此代碼非得氣死。這裏沒有一行goto,卻跳來跳去的比有goto的還難。

注意哦,咱們利用了next的第二個特性。resume 就是傳遞給callback 的函數,它所以接受了delay提供的值,resume傳遞這個值給 next, 故而 yield 語句返回了咱們的異步函數的結果。異步函數的結果因而被console打印出來。就像「倒脫靴」。

代碼整合起來:

run(function* myDelayedMessages(resume) {
    console.log(yield delay(1000, resume));
    console.log(yield delay(1200, resume));
})
//...waits 1000ms
// > "Slept for 1000"
//...waits 1200ms
// > "Slept for 1200"

就是這樣。咱們調用兩次delay,按照次序執行,卻沒有callback的嵌套。若是要調用12次,也仍是沒有callback嵌套。若是你依然疑惑不解,咱們再次分步來一遍》

  • run 以generator 爲參數,而且內部建立了一個resume 函數
  • run 建立一個generator函數,傳遞resume給它
  • 而後run第一次調用next,啓動了generator函數到yield
  • generator 遇到了第一個yield,並調用yield後的delay,而後暫停。
  • delay 在1000ms後完成,調用resume
  • resume 告訴generator 在走一步。而且傳遞delay的結果給run函數,由console 打印
  • generator 遇到第二個yield, 調用delay ,再次暫停
  • delay 在1200ms後完成,調用resume
  • resume 告訴generator 在走一步。而且傳遞delay的結果給run函數,由console 打印

co 更好

上面談到的作法,確實能夠把異步改爲同步了。但是並不太完美:好比resume顯得比較突兀,在好比只能在callback返回一個值。不夠通用。

這樣的話,能夠考慮TJ開發的co。連resume的聲明和引用也省掉了。仍是以delay爲例:

var co = require('co');
    function delay(time) {
      return function (callback){
        setTimeout(function () {//
          callback(null,"Slept for "+time);
        }, time);
      }
    }

    co(function *() {
      console.log(yield delay(1000))
      console.log(yield delay(1200))  
    })

爲了和co適配,delay須要作些修改,去掉callback,返回一個帶callback的函數,把計算結果經過callback傳遞出去。第一個參數依照node的規矩,留給err。

更絕。怪的不TJ被社區成爲大神。

再來一個。readFile(file,callback),做爲常見的異步函數如何修改?

var co = require('co');
    function readFile(file){
      return function (callback ){
        var fs = require("fs")
        fs.readFile(file,callback)
      }
    }

    co(function *() {
      console.log(yield readFile("./app.js"))
    })

    //<Buffer 76 61 72 20 63 ... >

但是,readFile改造這樣過工做,純粹就是boilerplate !因此,有人作了這樣的工做。安裝co-fs,就能夠:

co(function *() {
      var fs = require("co-fs")
      var js = yield fs.readFile('./app1.js', 'utf8')
      var files = yield fs.readdir('.')
      console.log(js,files)
    })

node.js真是玩梯雲縱。覺得已經很好了,仍是有人在加入一把火。

因此,值得去npm看看,查找下co-打頭的庫,有1000+個,要不要獨立出去:):

https://www.npmjs.com/search?q=co

打個總結

成功。咱們用generator替換了callback。咱們這樣作到的:

  • 建立一個run函數,以 generator 爲參數, 並傳遞一個 resume 函數給它
  • 傳遞resume 函數,單步推進generator ,返回任何異步callback得到的值給run
  • 傳遞resume 做爲異步調用callback . 這些異步函數一旦完成,就調用callback,所以就是調用resume。容許resume推進generator

generators替代「callback hell」 是否最佳是可爭論的,可是這個練習能夠幫助你理解到ES6的generators 和iterators

原文:http://modernweb.com/2014/02/10/replacing-callbacks-with-es6-generator...
參考:

Harmony Generator, yield, ES6, co框架學習 - http://bg.biedalian.com/2013/12/21/harmony-generator.html
Koa, co and coruntine - Harmony - 前端亂燉 - http://www.html-js.com/article/1752

fork me

fork me from :https://github.com/1000copy/learningnode/blob/master/nodebook/generato...

相關文章
相關標籤/搜索