鍍金池/ 教程/ HTML/ 使用 promise 替代回調(diào)函數(shù)
瀏覽器端測(cè)試:mocha,chai,phantomjs
搭建 Node.js 開發(fā)環(huán)境
測(cè)試用例:mocha,should,istanbul
線上部署:heroku
Mongodb 與 Mongoose 的使用
使用 superagent 與 cheerio 完成簡單爬蟲
js 中的那些最佳實(shí)踐
使用 eventproxy 控制并發(fā)
使用 promise 替代回調(diào)函數(shù)
作用域與閉包:this,var,(function () {})
持續(xù)集成平臺(tái):travis
測(cè)試用例:supertest
benchmark 怎么寫
使用 async 控制并發(fā)
學(xué)習(xí)使用外部模塊
一個(gè)最簡單的 express 應(yīng)用
正則表達(dá)式
cookie 和 session

使用 promise 替代回調(diào)函數(shù)

知識(shí)點(diǎn)

  1. 理解 Promise 概念,為什么需要 promise
  2. 學(xué)習(xí) q 的 API,利用 q 來替代回調(diào)函數(shù)(https://github.com/kriskowal/q )

課程內(nèi)容

第五課(https://github.com/alsotang/node-lessons/tree/master/lesson5 )講述了如何使用 async 來控制并發(fā)。async 的本質(zhì)是一個(gè)流程控制。其實(shí)在異步編程中,還有一個(gè)更為經(jīng)典的模型,叫做 Promise/Deferred 模型。

本節(jié)我們就來學(xué)習(xí)這個(gè)模型的代表實(shí)現(xiàn):q

首先,我們思考一個(gè)典型的異步編程模型,考慮這樣一個(gè)題目:讀取一個(gè)文件,在控制臺(tái)輸出這個(gè)文件內(nèi)容。

var fs = require('fs');
fs.readFile('sample.txt', 'utf8', function (err, data) {
    console.log(data);
});

看起來很簡單,再進(jìn)一步: 讀取兩個(gè)文件,在控制臺(tái)輸出這兩個(gè)文件內(nèi)容。

var fs = require('fs');
fs.readFile('sample01.txt', 'utf8', function (err, data) {
    console.log(data);
    fs.readFile('sample02.txt', 'utf8', function (err,data) {
        console.log(data);
    });
});

要是讀取更多的文件呢?

var fs = require('fs');
fs.readFile('sample01.txt', 'utf8', function (err, data) {
    fs.readFile('sample02.txt', 'utf8', function (err,data) {
        fs.readFile('sample03.txt', 'utf8', function (err, data) {
            fs.readFile('sample04.txt', 'utf8', function (err, data) {

            });
        });
    });
});

這段代碼就是臭名昭著的邪惡金字塔(Pyramid of Doom)??梢允褂胊sync來改善這段代碼,但是在本課中我們要用promise/defer來改善它。

promise基本概念

先學(xué)習(xí)promise的基本概念。

  • promise只有三種狀態(tài),未完成,完成(fulfilled)和失敗(rejected)。
  • promise的狀態(tài)可以由未完成轉(zhuǎn)換成完成,或者未完成轉(zhuǎn)換成失敗。
  • promise的狀態(tài)轉(zhuǎn)換只發(fā)生一次

promise有一個(gè)then方法,then方法可以接受3個(gè)函數(shù)作為參數(shù)。前兩個(gè)函數(shù)對(duì)應(yīng)promise的兩種狀態(tài)fulfilled, rejected的回調(diào)函數(shù)。第三個(gè)函數(shù)用于處理進(jìn)度信息。

promiseSomething().then(function(fulfilled){
        //當(dāng)promise狀態(tài)變成fulfilled時(shí),調(diào)用此函數(shù)
    },function(rejected){
        //當(dāng)promise狀態(tài)變成rejected時(shí),調(diào)用此函數(shù)
    },function(progress){
        //當(dāng)返回進(jìn)度信息時(shí),調(diào)用此函數(shù)
    });

學(xué)習(xí)一個(gè)簡單的例子:

var Q = require('q');
var defer = Q.defer();
/**
 * 獲取初始promise
 * @private
 */
function getInitialPromise() {
  return defer.promise;
}
/**
 * 為promise設(shè)置三種狀態(tài)的回調(diào)函數(shù)
 */
getInitialPromise().then(function(success){
    console.log(success);
},function(error){
    console.log(error);
},function(progress){
    console.log(progress);
});
defer.notify('in progress');//控制臺(tái)打印in progress
defer.resolve('resolve');   //控制臺(tái)打印resolve
defer.reject('reject');     //沒有輸出。promise的狀態(tài)只能改變一次

promise的傳遞

then方法會(huì)返回一個(gè)promise,在下面這個(gè)例子中,我們用outputPromise指向then返回的promise。

var outputPromise = getInputPromise().then(function (fulfilled) {
    }, function (rejected) {
    });

現(xiàn)在outputPromise就變成了受 function(fulfilled) 或者 function(rejected)控制狀態(tài)的promise了。怎么理解這句話呢?

  • 當(dāng)function(fulfilled)或者function(rejected)返回一個(gè)值,比如一個(gè)字符串,數(shù)組,對(duì)象等等,那么outputPromise的狀態(tài)就會(huì)變成fulfilled。

在下面這個(gè)例子中,我們可以看到,當(dāng)我們把inputPromise的狀態(tài)通過defer.resovle()變成fulfilled時(shí),控制臺(tái)輸出fulfilled.

當(dāng)我們把inputPromise的狀態(tài)通過defer.reject()變成rejected,控制臺(tái)輸出rejected

var Q = require('q');
var defer = Q.defer();
/**
 * 通過defer獲得promise
 * @private
 */
function getInputPromise() {
    return defer.promise;
}

/**
 * 當(dāng)inputPromise狀態(tài)由未完成變成fulfil時(shí),調(diào)用function(fulfilled)
 * 當(dāng)inputPromise狀態(tài)由未完成變成rejected時(shí),調(diào)用function(rejected)
 * 將then返回的promise賦給outputPromise
 * function(fulfilled) 和 function(rejected) 通過返回字符串將outputPromise的狀態(tài)由
 * 未完成改變?yōu)閒ulfilled
 * @private
 */
var outputPromise = getInputPromise().then(function(fulfilled){
    return 'fulfilled';
},function(rejected){
    return 'rejected';
});

/**
 * 當(dāng)outputPromise狀態(tài)由未完成變成fulfil時(shí),調(diào)用function(fulfilled),控制臺(tái)打印'fulfilled: fulfilled'。
 * 當(dāng)outputPromise狀態(tài)由未完成變成rejected, 調(diào)用function(rejected), 控制臺(tái)打印'fulfilled: rejected'。
 */
outputPromise.then(function(fulfilled){
    console.log('fulfilled: ' + fulfilled);
},function(rejected){
    console.log('rejected: ' + rejected);
});

/**
 * 將inputPromise的狀態(tài)由未完成變成rejected
 */
defer.reject(); //輸出 fulfilled: rejected

/**
 * 將inputPromise的狀態(tài)由未完成變成fulfilled
 */
//defer.resolve(); //輸出 fulfilled: fulfilled
  • 當(dāng)function(fulfilled)或者function(rejected)拋出異常時(shí),那么outputPromise的狀態(tài)就會(huì)變成rejected
var Q = require('q');
var fs = require('fs');
var defer = Q.defer();

/**
 * 通過defer獲得promise
 * @private
 */
function getInputPromise() {
    return defer.promise;
}

/**
 * 當(dāng)inputPromise狀態(tài)由未完成變成fulfil時(shí),調(diào)用function(fulfilled)
 * 當(dāng)inputPromise狀態(tài)由未完成變成rejected時(shí),調(diào)用function(rejected)
 * 將then返回的promise賦給outputPromise
 * function(fulfilled) 和 function(rejected) 通過拋出異常將outputPromise的狀態(tài)由
 * 未完成改變?yōu)閞eject
 * @private
 */
var outputPromise = getInputPromise().then(function(fulfilled){
    throw new Error('fulfilled');
},function(rejected){
    throw new Error('rejected');
});

/**
 * 當(dāng)outputPromise狀態(tài)由未完成變成fulfil時(shí),調(diào)用function(fulfilled)。
 * 當(dāng)outputPromise狀態(tài)由未完成變成rejected, 調(diào)用function(rejected)。
 */
outputPromise.then(function(fulfilled){
    console.log('fulfilled: ' + fulfilled);
},function(rejected){
    console.log('rejected: ' + rejected);
});

/**
 * 將inputPromise的狀態(tài)由未完成變成rejected
 */
defer.reject();     //控制臺(tái)打印 rejected [Error:rejected]

/**
 * 將inputPromise的狀態(tài)由未完成變成fulfilled
 */
//defer.resolve(); //控制臺(tái)打印 rejected [Error:fulfilled]
  • 當(dāng)function(fulfilled)或者function(rejected)返回一個(gè)promise時(shí),outputPromise就會(huì)成為這個(gè)新的promise.

這樣做有什么意義呢? 主要在于聚合結(jié)果(Q.all),管理延時(shí),異?;謴?fù)等等

比如說我們想要讀取一個(gè)文件的內(nèi)容,然后把這些內(nèi)容打印出來??赡軙?huì)寫出這樣的代碼:

//錯(cuò)誤的寫法
var outputPromise = getInputPromise().then(function(fulfilled){
    fs.readFile('test.txt','utf8',function(err,data){
        return data;
    });
});

然而這樣寫是錯(cuò)誤的,因?yàn)閒unction(fulfilled)并沒有返回任何值。需要下面的方式:

var Q = require('q');
var fs = require('fs');
var defer = Q.defer();

/**
 * 通過defer獲得promise
 * @private
 */
function getInputPromise() {
    return defer.promise;
}

/**
 * 當(dāng)inputPromise狀態(tài)由未完成變成fulfil時(shí),調(diào)用function(fulfilled)
 * 當(dāng)inputPromise狀態(tài)由未完成變成rejected時(shí),調(diào)用function(rejected)
 * 將then返回的promise賦給outputPromise
 * function(fulfilled)將新的promise賦給outputPromise
 * 未完成改變?yōu)閞eject
 * @private
 */
var outputPromise = getInputPromise().then(function(fulfilled){
    var myDefer = Q.defer();
    fs.readFile('test.txt','utf8',function(err,data){
        if(!err && data) {
            myDefer.resolve(data);
        }
    });
    return myDefer.promise;
},function(rejected){
    throw new Error('rejected');
});

/**
 * 當(dāng)outputPromise狀態(tài)由未完成變成fulfil時(shí),調(diào)用function(fulfilled),控制臺(tái)打印test.txt文件內(nèi)容。
 *
 */
outputPromise.then(function(fulfilled){
    console.log(fulfilled);
},function(rejected){
    console.log(rejected);
});

/**
 * 將inputPromise的狀態(tài)由未完成變成rejected
 */
//defer.reject();

/**
 * 將inputPromise的狀態(tài)由未完成變成fulfilled
 */
defer.resolve(); //控制臺(tái)打印出 test.txt 的內(nèi)容

方法傳遞

方法傳遞有些類似于Java中的try和catch。當(dāng)一個(gè)異常沒有響應(yīng)的捕獲時(shí),這個(gè)異常會(huì)接著往下傳遞。

方法傳遞的含義是當(dāng)一個(gè)狀態(tài)沒有響應(yīng)的回調(diào)函數(shù),就會(huì)沿著then往下找。

  • 沒有提供function(rejected)
var outputPromise = getInputPromise().then(function(fulfilled){})

如果inputPromise的狀態(tài)由未完成變成rejected, 此時(shí)對(duì)rejected的處理會(huì)由outputPromise來完成。

var Q = require('q');
var fs = require('fs');
var defer = Q.defer();

/**
 * 通過defer獲得promise
 * @private
 */
function getInputPromise() {
    return defer.promise;
}

/**
 * 當(dāng)inputPromise狀態(tài)由未完成變成fulfil時(shí),調(diào)用function(fulfilled)
 * 當(dāng)inputPromise狀態(tài)由未完成變成rejected時(shí),這個(gè)rejected會(huì)傳向outputPromise
 */
var outputPromise = getInputPromise().then(function(fulfilled){
    return 'fulfilled'
});
outputPromise.then(function(fulfilled){
    console.log('fulfilled: ' + fulfilled);
},function(rejected){
    console.log('rejected: ' + rejected);
});

/**
 * 將inputPromise的狀態(tài)由未完成變成rejected
 */
defer.reject('inputpromise rejected'); //控制臺(tái)打印rejected: inputpromise rejected

/**
 * 將inputPromise的狀態(tài)由未完成變成fulfilled
 */
//defer.resolve();
  • 沒有提供function(fulfilled)
var outputPromise = getInputPromise().then(null,function(rejected){})

如果inputPromise的狀態(tài)由未完成變成fulfilled, 此時(shí)對(duì)fulfil的處理會(huì)由outputPromise來完成。

var Q = require('q');
var fs = require('fs');
var defer = Q.defer();

/**
 * 通過defer獲得promise
 * @private
 */
function getInputPromise() {
    return defer.promise;
}

/**
 * 當(dāng)inputPromise狀態(tài)由未完成變成fulfil時(shí),傳遞給outputPromise
 * 當(dāng)inputPromise狀態(tài)由未完成變成rejected時(shí),調(diào)用function(rejected)
 * function(fulfilled)將新的promise賦給outputPromise
 * 未完成改變?yōu)閞eject
 * @private
 */
var outputPromise = getInputPromise().then(null,function(rejected){
    return 'rejected';
});

outputPromise.then(function(fulfilled){
    console.log('fulfilled: ' + fulfilled);
},function(rejected){
    console.log('rejected: ' + rejected);
});

/**
 * 將inputPromise的狀態(tài)由未完成變成rejected
 */
//defer.reject('inputpromise rejected');

/**
 * 將inputPromise的狀態(tài)由未完成變成fulfilled
 */
defer.resolve('inputpromise fulfilled'); //控制臺(tái)打印fulfilled: inputpromise fulfilled
  • 可以使用fail(function(error))來專門針對(duì)錯(cuò)誤處理,而不是使用then(null,function(error))
 var outputPromise = getInputPromise().fail(function(error){})

看這個(gè)例子

var Q = require('q');
var fs = require('fs');
var defer = Q.defer();

/**
 * 通過defer獲得promise
 * @private
 */
function getInputPromise() {
    return defer.promise;
}

/**
 * 當(dāng)inputPromise狀態(tài)由未完成變成fulfil時(shí),調(diào)用then(function(fulfilled))
 * 當(dāng)inputPromise狀態(tài)由未完成變成rejected時(shí),調(diào)用fail(function(error))
 * function(fulfilled)將新的promise賦給outputPromise
 * 未完成改變?yōu)閞eject
 * @private
 */
var outputPromise = getInputPromise().then(function(fulfilled){
    return fulfilled;
}).fail(function(error){
    console.log('fail: ' + error);
});
/**
 * 將inputPromise的狀態(tài)由未完成變成rejected
 */
defer.reject('inputpromise rejected');//控制臺(tái)打印fail: inputpromise rejected

/**
 * 將inputPromise的狀態(tài)由未完成變成fulfilled
 */
//defer.resolve('inputpromise fulfilled');
  • 可以使用progress(function(progress))來專門針對(duì)進(jìn)度信息進(jìn)行處理,而不是使用 then(function(success){},function(error){},function(progress){})
var Q = require('q');
var defer = Q.defer();
/**
 * 獲取初始promise
 * @private
 */
function getInitialPromise() {
  return defer.promise;
}
/**
 * 為promise設(shè)置progress信息處理函數(shù)
 */
var outputPromise = getInitialPromise().then(function(success){

}).progress(function(progress){
    console.log(progress);
});

defer.notify(1);
defer.notify(2); //控制臺(tái)打印1,2

promise鏈

promise鏈提供了一種讓函數(shù)順序執(zhí)行的方法。

函數(shù)順序執(zhí)行是很重要的一個(gè)功能。比如知道用戶名,需要根據(jù)用戶名從數(shù)據(jù)庫中找到相應(yīng)的用戶,然后將用戶信息傳給下一個(gè)函數(shù)進(jìn)行處理。

var Q = require('q');
var defer = Q.defer();

//一個(gè)模擬數(shù)據(jù)庫
var users = [{'name':'andrew','passwd':'password'}];

function getUsername() {
return defer.promise;
}

function getUser(username){
    var user;
    users.forEach(function(element){
        if(element.name === username) {
            user = element;
        }
    });
    return user;
}

//promise鏈
getUsername().then(function(username){
 return getUser(username);
}).then(function(user){
 console.log(user);
});

defer.resolve('andrew');

我們通過兩個(gè)then達(dá)到讓函數(shù)順序執(zhí)行的目的。

then的數(shù)量其實(shí)是沒有限制的。當(dāng)然,then的數(shù)量過多,要手動(dòng)把他們鏈接起來是很麻煩的。比如

foo(initialVal).then(bar).then(baz).then(qux)

這時(shí)我們需要用代碼來動(dòng)態(tài)制造promise鏈

var funcs = [foo,bar,baz,qux]
var result = Q(initialVal)
funcs.forEach(function(func){
    result = result.then(func)
})
return result

當(dāng)然,我們可以再簡潔一點(diǎn)

var funcs = [foo,bar,baz,qux]
funcs.reduce(function(pre,current),Q(initialVal){
    return pre.then(current)
})

看一個(gè)具體的例子

function foo(result) {
    console.log(result);
    return result+result;
}
//手動(dòng)鏈接
Q('hello').then(foo).then(foo).then(foo);                                   //控制臺(tái)輸出: hello
                                                                            //             hellohello
                                                                            //             hellohellohello

//動(dòng)態(tài)鏈接
var funcs = [foo,foo,foo];
var result = Q('hello');
funcs.forEach(function(func){
    result = result.then(func);
});
//精簡后的動(dòng)態(tài)鏈接
funcs.reduce(function(prev,current){
    return prev.then(current);
},Q('hello'));

對(duì)于promise鏈,最重要的是需要理解為什么這個(gè)鏈能夠順序執(zhí)行。如果能夠理解這點(diǎn),那么以后自己寫promise鏈可以說是輕車熟路啊。

promise組合

回到我們一開始讀取文件內(nèi)容的例子。如果現(xiàn)在讓我們把它改寫成promise鏈,是不是很簡單呢?

var Q = require('q'),
    fs = require('fs');
function printFileContent(fileName) {
    return function(){
        var defer = Q.defer();
        fs.readFile(fileName,'utf8',function(err,data){
          if(!err && data) {
            console.log(data);
            defer.resolve();
          }
        })
        return defer.promise;
    }
}
//手動(dòng)鏈接
printFileContent('sample01.txt')()
    .then(printFileContent('sample02.txt'))
    .then(printFileContent('sample03.txt'))
    .then(printFileContent('sample04.txt'));   //控制臺(tái)順序打印sample01到sample04的內(nèi)容

很有成就感是不是。然而如果仔細(xì)分析,我們會(huì)發(fā)現(xiàn)為什么要他們順序執(zhí)行呢,如果他們能夠并行執(zhí)行不是更好嗎? 我們只需要在他們都執(zhí)行完成之后,得到他們的執(zhí)行結(jié)果就可以了。

我們可以通過Q.all([promise1,promise2...])將多個(gè)promise組合成一個(gè)promise返回。 注意:

  1. 當(dāng)all里面所有的promise都fulfil時(shí),Q.all返回的promise狀態(tài)變成fulfil
  2. 當(dāng)任意一個(gè)promise被reject時(shí),Q.all返回的promise狀態(tài)立即變成reject

我們來把上面讀取文件內(nèi)容的例子改成并行執(zhí)行吧

var Q = require('q');
var fs = require('fs');
/**
 *讀取文件內(nèi)容
 *@private
 */
function printFileContent(fileName) {
        //Todo: 這段代碼不夠簡潔??梢允褂肣.denodeify來簡化
        var defer = Q.defer();
        fs.readFile(fileName,'utf8',function(err,data){
          if(!err && data) {
            console.log(data);
            defer.resolve(fileName + ' success ');
          }else {
            defer.reject(fileName + ' fail ');
          }
        })
        return defer.promise;
}

Q.all([printFileContent('sample01.txt'),printFileContent('sample02.txt'),printFileContent('sample03.txt'),printFileContent('sample04.txt')])
    .then(function(success){
        console.log(success);
    }); //控制臺(tái)打印各個(gè)文件內(nèi)容 順序不一定

現(xiàn)在知道Q.all會(huì)在任意一個(gè)promise進(jìn)入reject狀態(tài)后立即進(jìn)入reject狀態(tài)。如果我們需要等到所有的promise都發(fā)生狀態(tài)后(有的fulfil, 有的reject),再轉(zhuǎn)換Q.all的狀態(tài), 這時(shí)我們可以使用Q.allSettled

var Q = require('q'),
    fs = require('fs');
/**
 *讀取文件內(nèi)容
 *@private
 */
function printFileContent(fileName) {
    //Todo: 這段代碼不夠簡潔??梢允褂肣.denodeify來簡化
    var defer = Q.defer();
    fs.readFile(fileName,'utf8',function(err,data){
      if(!err && data) {
        console.log(data);
        defer.resolve(fileName + ' success ');
      }else {
        defer.reject(fileName + ' fail ');
      }
    })
    return defer.promise;
}

Q.allSettled([printFileContent('nosuchfile.txt'),printFileContent('sample02.txt'),printFileContent('sample03.txt'),printFileContent('sample04.txt')])
    .then(function(results){
        results.forEach(
            function(result) {
                console.log(result.state);
            }
        );
    });

結(jié)束promise鏈

通常,對(duì)于一個(gè)promise鏈,有兩種結(jié)束的方式。第一種方式是返回最后一個(gè)promise

return foo().then(bar);

第二種方式就是通過done來結(jié)束promise鏈

foo().then(bar).done()

為什么需要通過done來結(jié)束一個(gè)promise鏈呢? 如果在我們的鏈中有錯(cuò)誤沒有被處理,那么在一個(gè)正確結(jié)束的promise鏈中,這個(gè)沒被處理的錯(cuò)誤會(huì)通過異常拋出。

var Q = require('q');
/**
 *@private
 */
function getPromise(msg,timeout,opt) {
    var defer = Q.defer();
    setTimeout(function(){
    console.log(msg);
        if(opt)
            defer.reject(msg);
        else
            defer.resolve(msg);
    },timeout);
    return defer.promise;
}
/**
 *沒有用done()結(jié)束的promise鏈
 *由于getPromse('2',2000,'opt')返回rejected, getPromise('3',1000)就沒有執(zhí)行
 *然后這個(gè)異常并沒有任何提醒,是一個(gè)潛在的bug
 */
getPromise('1',3000)
    .then(function(){return getPromise('2',2000,'opt')})
    .then(function(){return getPromise('3',1000)});
/**
 *用done()結(jié)束的promise鏈
 *有異常拋出
 */
getPromise('1',3000)
    .then(function(){return getPromise('2',2000,'opt')})
    .then(function(){return getPromise('3',1000)})
    .done();

結(jié)束語

當(dāng)你理解完上面所有的知識(shí)點(diǎn)時(shí),你就會(huì)正確高效的使用promise了。本節(jié)只是講了promise的原理和幾個(gè)基本的API,不過你掌握了這些之后,再去看q的文檔,應(yīng)該很容易就能理解各個(gè)api的意圖。