使用yield進行異步流程控制

現狀

目前咱們對異步回調的解決方案有這麼幾種:回調,deferred/promise和事件觸發。回調的方式自沒必要說,須要硬編碼調用,並且有可能會出現複雜的嵌套關係,形成「回調黑洞」;deferred/promise方式則對使用者而言簡潔明瞭,在執行異步函數以前就已經構造好了執行鏈--then鏈,並且實現也很靈活,具體可參考Promise的實現;事件機制則是一種觀察者模式的實現,但也必須硬編碼在異步執行的函數中,當異步函數執行完畢後再trigger相關事件,而觀察者則相應執行事件處理函數。
注意,剛剛提到了一個詞--硬編碼,依賴這種方式僅實現回調侷限性很大,如在node中,對fs.readFile('file1','utf-8')完成以後再進行fs.readFile('file2','utf-8'),使用回調和事件觸發則必須在第一個異步的回調函數中進行調用trigger,加強了這兩個操做的強依賴,使用deferred/promise則會很好的避免。
如今,隨着ECMAScript6的逐漸普及,咱們能夠在chrome和node端嘗試一種新的異步流程控制--generator。經過generator,咱們能夠控制函數內部的執行階段,進而能夠利用高階函數的特性進行擴展,完成對異步流程的控制。html

特性及兼容性

因爲隸屬於ECMAScript6規範,所以兼容性是一個大問題,不過咱們在最新版的chrome和node --harmony下使用該功能,因此作node端開發的小夥伴們能夠大膽的使用。
那麼,什麼是generator呢?
function* (){}
這就是一個匿名的generator。經過function關鍵字和函數名或者括號之間添加「*」定義一個generator函數,咱們也能夠這樣判斷一個函數是否爲generator:
typeof fn == 'function' && fn.constructor.name == 'GeneratorFunction'
在generator中咱們能夠關鍵字yield,java程序員對yield確定不陌生,yield在java中是線程調度的一種方式,能夠釋放時間片讓同級別的線程執行,然而在js中,yield卻大不相同,由於js的執行線程是單線程,因此調度就不存在,yield咱們能夠理解爲函數執行的一個斷點,每次只能執行到yield處,這樣本來順序或者異步執行的函數邏輯均可以經過某種方式使他們以順序的方式呈如今咱們眼前,在這裏須要強調下,經過yield只能斷點執行generator函數中的邏輯,在函數以外並不會阻塞,不然整個主線程就會掛掉
一個generator函數執行到yield處,咱們經過調用generator object的next()繼續進行,generator object(下文簡寫爲GO)就是generator函數的返回對象,調用GO的next方法會返回一個{value: '',done: false}這樣的對象,value爲yield關鍵字後面的表達式的值,done則表示generator函數是否執行完畢。
這就是基本的generator全部的數據結構,很簡單明瞭。前端

實例

function * fn(){
 var a = yield 1;
 console.log(a);
 var b = yield 2;
 console.log(b);
}
var go = fn(); // 這是一個generator object
go.next(); // 執行到第一個 yield ,執行表達式 1
go.next(); //執行到第二個yield,輸出console.log(a)爲undefined,執行表達式 2
go.next(); //執行console.log(b),輸出 undefined

上面的demo很容易理解,可能惟一有點疑問的就是console.log的輸出。這裏強調,每次next,只執行yield後面的表達式,這樣對於前面的賦值操做就無能爲力,那麼如何對a進行賦值呢?能夠經過第二個next進行傳值。經過對第二個go.next(2),這樣a的值就被賦爲2,同理b的值也能夠這樣傳遞。
可是,這對於異步流程控制有什麼用呢?其實,仍是經過分段執行異步操做來完成。每一個yield async1()執行完畢,將結果做爲參數傳給下一個yield async2(),這樣咱們只需判斷GO.done是否爲true來終止這個流程。java

異步流程控制

咱們的目標是實現這種方式的流程控制:node

flow(function *(){
 var readFile = helper(fs.readFile);
 var t1 = yield readFile('./files/f1', 'utf8');
 var t2 = yield readFile(t1, 'utf8');
 console.log(t2);
});

其中flow是流程控制函數,參數爲一個generator,helper函數則是一個包裝函數,負責針對異步操做進行處理,下面咱們看看helper函數的邏輯。程序員

var helper = function(fn) {
  var feed; // 用於存儲回調函數,該函數複用於全部用於helper處理的異步函數
  /**
   * 執行次序分析:
   *    helper的參數fn是一個異步函數,經過helper的處理,返回一個含有內部處理邏輯
   *    的函數,該函數封裝了所需參數和可能的回調函數feed,而且返回一個設置feed的函數。
   *
   *    在具體的使用中,經過helper函數封裝fs.readFile,獲取readFile。當執行第一個
   *    片斷時,首先將全部的參數(包括feed)合併到args,並執行異步調用返回處理函數;此時
   *    咱們用獲取的返回函數設置回調函數,進而影響到args中的最後一項的函數
   */

  return function(){
    var args = [].slice.call(arguments);

    args.push(function(){
      if(feed) {
          feed.apply(null,arguments);
      }
      console.log(feed)
    });
    fn.apply(null,args);
    // 返回一個函數,用於給yield以前的變量賦值
    return function(fn){
      feed = fn;
    }
};

helper函數的做用就是從新包裝異步函數,返回的包裝函數也會返回一個函數,用於給回調函數feed賦值。
全部的異步函數都須要用helper進行封裝,已傳遞必要的回調,最後按照flow分發的流程「依次執行」。
下面咱們實現flow的控制邏輯:chrome

var flow = function(gfn) {
var generator = gfn();
next();

function next(data){
    generator.ret = generator.next(data);
    if(generator.ret.done){
        return;
    }
    generator.ret.value(
        function(error,d){
            if(error)
                throw error;

            next.apply(null,[].slice.call(arguments,1));
        }
     );
  }
};

邏輯依舊很簡單,針對傳入的generator生產generator object,最後進入next遞歸。在遞歸中,首先執行next邏輯並判斷是否到了generator的終點,若是沒有則調用generator object的value方法(此處爲「被helper處理過得函數的返回值,即function(fn){feed = fn}」)對回調進行賦值,在回調中則遞歸執行next函數,直至generator結束邏輯。
經過這樣的方式,咱們制定了flow流程,能夠將多個異步操做順序執行,而不影響generator函數以外的其他邏輯,這樣避免了硬編碼,沒有了回調黑洞,咱們只需在異步函數前加yield便可,省時省事。express

flow(function *(){
 var readFile = helper(fs.readFile);
 var nt = helper(process.nextTick);
 var t1 = yield readFile('./files/f1', 'utf8');
 var t2 = yield readFile(t1, 'utf8');
 yield nt(function(){console.log(t2)});
//  console.log(t2);
});

能夠用helper封裝各類異步回調,在具體的業務邏輯中傳入其他回調返回值做爲參數,從而達到目的。數組

並行異步執行

yield 後面不只僅能夠放置表達式,也能夠放置數組。數組的每項爲表達式,這樣每次執行到yield時,會並行執行這些異步操做,返回對象的value屬性也是一個數組,咱們依舊能夠對value數組的每項進行賦值,從而完成回調的賦值。promise

var length = generator.ret.value.length,
ret = [];
generator.ret.value.forEach(function(item,i){

  item(function(err,data) {
      --length;
      if (err) {
        console.log(err.message);
        //  throw err;
      }
      ret.push(data);
      if(0 == length){
        generator.next(ret);
      }
  });

});

對value值進行遍歷,並判斷並行的異步操做是否都已完成,若完成則傳遞ret數組給變量。數據結構

throw特性

這塊throw語法糖是後來添加的,之因此提到它是由於它的表現有點獨特:

var gen = function* gen() {
  try {
    yield console.log('hello');
    yield console.log('world');
  }
  catch (e) {
    console.log(e);
    yield console.log('error...');
  }
  yield console.log('end');
}

var g = gen();

g.next(); 
g.throw('a');
g.next();

第一個next後,輸出‘hello’;
throw後,輸出‘a’、‘error...’
第二個next後,輸出‘end’
能夠發現gen.throw後,不只執行到catch代碼塊,並且還會執行下一個yield表達式,在這裏須要注意下!

應用

目前generator的兼容性要求其只能在node平臺上使用,目前express框架的後繼者koa採用了generator實現中間件的方式,中間件處理完每一個請求都會經過yield *next的方式進行分發,此處的next也是一個generator object,經過yield *next的方式能夠嵌套多層generator鏈,這樣next()就會到下一個generator的yield處。 分解函數的執行,這種方式確實讓人耳目一新,咱們有理由相信js的將來,咱們要堅信js將來的能量,咱們要自豪咱們處在前端開發這個領域內。

相關文章
相關標籤/搜索