利用generator(thunk化函數/promise方法)處理回調地獄的問題

在Nodejs中是通過回調函數控制異步過程的,但是當多個事件之間相互依賴,或多個事件一起協作時,就會導致函數嵌套過深的情況。如下是一個在項目中顯示文章詳情頁的的代碼,我們看到這個函數嵌套了七八層。這樣的嵌套很容易因爲缺少括號發生錯誤。另外,當一個回調函數中出現錯誤,整個程序就會退出,並提示錯誤信息。

        //查看文章詳細信息
app.get("/detail/:author/:title",function(req,res){

var isAgree = false;
var isColl = false;
var isAttention = false;
console.log(req.session.user);
Post.getOne({author: req.params.author,title: req.params.title}, function (err, post) {
  if (err) {
    req.flash('error', err); 

    console.log(err);
  }
  //判斷是否已點贊
  if ( req.session.user && post.agree &&post.agree.indexOf(req.session.user.email)>=0 ) {
    isAgree = true;
  }
  //判斷是否已收藏
  if ( req.session.user && post.postcoll.indexOf(req.session.user.email)>=0 ) {
    isColl = true;
  }
  User.getOne({email:req.params.author},function(err,author_detail){
    if( req.session.user && req.session.user.attention && req.session.user.attention.indexOf(req.params.author) >=0){
      isAttention = true;
    }
    console.log(isAttention);
  //獲取作者的頭像(暱稱的問題)
    Post.getTen({tags:{$in:post.tags}},1,{pv:-1},function(err, posts, totle,userImg){
      if ( err ){
        console.log(err);
      }
      Post.countPost({author:req.session.user.email},function(err,count){
        if(err){
          console.log(err);
         }
      //訪問量增加

      Post.viewNum( {author: req.params.author,title: req.params.title},function(err){
        Post.getArchive({author:req.session.user.email},function(err,docs){
          console.log(docs);
          res.render('post/showPost', {
            title: req.params.title,
            post: post,
            relate: posts,
            cates: docs,
            count:count,
            user: req.session.user,
            author_detail:author_detail,
            isAgree : isAgree,
            isColl: isColl,
            isAttention:isAttention
          })
        });
      })
      })
    }) 
  })     
});
 })

爲了解決函數嵌套過深的問題,現有的方案有模塊化、promise方法和generator方法。該文主要介紹generator方法。

generator簡介

generator是ES6新增的一個生成器,是一種特殊的函數。與普通函數不同的是,定義時需要在函數名前添加‘*’,並且在調用Generator函數後,該函數並不執行,返回的也不是函數的運行結果,而是一個指向內部狀態的指針對象。當調用指針對象的next()方法時,指針指向下一個狀態。也就是說,每次調用next方法,內部指針就從函數頭部或上一次停下開的地方開始執行,直到遇到下一條yield語句(或return語句)爲止。generator是分段進行的,yield語句是暫停執行的標記,而next方法可以恢復執行

function* generatorFunc(){
    var i = 0 ;
    while(i < 5 ){
        yield i;

        i++
    }
}

var gene = generatorFunc();//Object {value: 0, done: false}
console.log(gene.next());//Object {value: 1, done: false}
console.log(gene.next());//Object {value: 2, done: false}
console.log(gene.next());//Object {value: 3, done: false}
console.log(gene.next());//Object {value: 4, done: false}
console.log(gene.next());//Object {value: undefined, done: true}
console.log(gene.next());//函數已經結束 無輸出

這裏說一下yield語句,yield語句就是函數的暫停標誌。運行邏輯如下

  • 函數調用next方法,遇到yield語句就暫停執行後面的操作,並將緊跟在yield後的表達式的值作爲next方法返回對象的value值。注意yield語句本身沒有返回值,或者說總返回undefined。next方法可以帶一個參數,該參數會被當做上一條yield語句的返回值.

        function* generatorFunc(){
        var i = 0 ;
        while(i < 5 ){
            var rest = yield i;
            console.log(rest);
            i++
        }
        }
    
        var gene = generatorFunc();
        console.log(gene.next());//Object {value: 0, done: false} rest輸出undefined
        console.log(gene.next('ccc'));//Object {value: 0, done: false} rest輸出‘ccc’
    
  • 下一次調用next方法時再繼續向下執行,直到遇到下一個yield語句
  • 如果沒有遇到新的yield語句,就一直運行到函數結束,直到return位置,並將return語句後面的表達式的值作爲返回的對象的value值
  • 如果該函數沒有return語句,則返回的對象的value屬性爲undefined。

generator結合thunk化函數處理回調地獄

Nodejs中co庫實現了使用generator處理回調函數的問題。KOA框架主要使用的也是generator原理處理的異步函數。這裏介紹一下大概的原理。
我們先創建使用回調函數實現讀取文件的功能

var fs = require("fs");
fs.readFile('path1', function (err, data) {
  if (err) throw err;
  console.log(data);
  fs.readFile('path2', function (err, data) {
    if (err) throw err;
        console.log(data);
  });
});

現在我們藉助co的思想使用generator來改寫它

var fs = require('fs');
co(function* (){
    var data1 = yield readFile('test.txt');
    console.log(data1);
    var data2 = yield readFile('test1.txt');
    console.log(data2);
})

// Thunk 化就是將多參數函數,將其替換成單參數只接受回調函數作爲唯一參數的版本 
function readFile( path ){
    return function(callback){
        fs.readFile(path,callback);
    }
}

function co(fn){
    var gen = fn();
    function next(err,data){
        var result = gen.next(data);
        if(!result.done){
            console.log(result);
            result.value(next);
        }
    }
    next();
}

讓我們分析一下上面的代碼。co函數的參數需要是一個生成器函數。在co中定義一個next方法,該方法主要調用生成器函數中的next方法。next方法中傳遞的參數即爲生成器函數上一條yield語句的返回值(對應data1和data2)。result中的value爲next方法返回的值(包含value和done兩個值,當done爲true時,表示已經函數執行完成)。當函數沒執行完成時調用value中的函數。value中的函數是被thunk化的,只接受回調函數。
上面代碼有兩個關鍵點,一個是readfile函數的thunk化,還有就是co函數。
有一點需要注意,co中需要流程控制的函數,都必須要Thunk化或者Promise化。所謂Thunk化就是將多參數函數替換隻接收回調函數的單參數函數。原生API不支持Thunk化,所以就有了thunkify這個庫將原生api thunk化。
爲什麼要thunk化呢,利用generator來實現異步回調的實質就是把next()放入回調函數中。thunk化之後可以得到一個只接收callback的函數。

generator結合promise對象解決回調地獄問題

promise對象其實是一個用來處理異步操作的延遲對象。promise對象有三種狀態:未完成態、完成態和失敗態,其中未完成態可以轉化爲完成態和失敗態,但是不能逆向轉化也不能相互轉化,即完成態(失敗態)不能轉化爲未完成態和失敗態(完成態)。只有異步的結果可以決定當前是那種狀態,任何其他操作都不能改變這種狀態。
讓我們大概瞭解一下promise的基本用法

  • 首先需要創建Promise對象,Promise構造函數接收一個函數作爲參數,該函數的兩個參數分別爲resolve和reject。resolve的作用是當Promise對象的狀態從未完成態變爲完成態的時候調用,reject的作用是當Promise對象從未完成態變爲失敗態的時候調用。

    var promise = new Promise(function(resolve,reject){});
    
  • Promise實例的then方法可以分別指定resolve和reject轉檯的回調函數

     promise.then(function(value){
         //success
     },function(err){
         //failure
     });
    

下面我們看一下如何使用promise和generator解決回調地獄的問題。先上代碼

var Q = require('q');
var preadFile = function(file){
    var deferred = Q.defer();
    fs.readFile(file,function(err,data){
        if(!err){
            deferred.resolve(data);
        }else{
            deferred.reject(err);
        }
        console.log(deferred.promise);

    })
    return deferred.promise;

}
//建立generator生成器函數
var g = function* (){
    try{
        //執行創建promise對象的函數 
        var foo = yield preadFile('test.txt');
        console.log(foo);
    }catch(e){
        console.log(e);
    }
}
//創建執行generator生成器的函數
function run(generator) {
    var it = generator();
    //result的值爲執行生成器的next方法返回的值,含有value和done兩個屬性,value爲promise對象
    function go (result){
    //當result中的done取值爲true時,表示該異步操作已經執行完成。
        if(result.done){
            return result.value;
        } 
        //設置promise對象的then方法
        return result.value.then(function(data){
            return go(it.next(data));
        },function(error){
            return go(it.throw(error));
        })
    }
    go(it.next());
}

run(g);

從代碼中可以看出。該方法主要分爲三部分,首先創建一個返回promise對象的函數getFoo;創建generator生成器函數,並在yield語句中執行getFoo函數,使generator執行next方法的時候返回promise對象,當接收到promise對象時,給對象添加then方法。最重要的是在then方法中執行nextgenerator的next方法。這些函數中最主要的是run方法,在run方法中運行了控制異步流程的generator方法,並定義了promise的then方法。
最後總結一下:歸根結底,使用generator解決回調地獄問題,需要首先處理一下調用的函數,使函數正確執行後能夠自動執行next方法,並且傳遞執行完方法後的結果。

參考文獻:co.js 異步回調的原理

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章