## 知識點
1. 理解 Promise 概念,為什么需要 promise
2. 學習 q 的 API,利用 q 來替代回調函數([https://github.com/kriskowal/q](https://github.com/kriskowal/q)?)
## [](https://github.com/alsotang/node-lessons/tree/master/lesson17#課程內容)課程內容
第五課講述了如何使用 async 來控制并發。async 的本質是一個流程控制。其實在異步編程中,還有一個更為經典的模型,叫做 Promise/Deferred 模型。
本節我們就來學習這個模型的代表實現:[q](https://github.com/kriskowal/q)
首先,我們思考一個典型的異步編程模型,考慮這樣一個題目:讀取一個文件,在控制臺輸出這個文件內容。
~~~
var fs = require('fs');
fs.readFile('sample.txt', 'utf8', function (err, data) {
console.log(data);
});
~~~
看起來很簡單,再進一步: 讀取兩個文件,在控制臺輸出這兩個文件內容。
~~~
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)。可以使用async來改善這段代碼,但是在本課中我們要用promise/defer來改善它。
## [](https://github.com/alsotang/node-lessons/tree/master/lesson17#promise基本概念)promise基本概念
先學習promise的基本概念。
* promise只有三種狀態,未完成,完成(fulfilled)和失敗(rejected)。
* promise的狀態可以由未完成轉換成完成,或者未完成轉換成失敗。
* promise的狀態轉換只發生一次
promise有一個then方法,then方法可以接受3個函數作為參數。前兩個函數對應promise的兩種狀態fulfilled, rejected的回調函數。第三個函數用于處理進度信息。
~~~
promiseSomething().then(function(fulfilled){
//當promise狀態變成fulfilled時,調用此函數
},function(rejected){
//當promise狀態變成rejected時,調用此函數
},function(progress){
//當返回進度信息時,調用此函數
});
~~~
學習一個簡單的例子:
~~~
var Q = require('q');
var defer = Q.defer();
/**
* 獲取初始promise
* @private
*/
function getInitialPromise() {
return defer.promise;
}
/**
* 為promise設置三種狀態的回調函數
*/
getInitialPromise().then(function(success){
console.log(success);
},function(error){
console.log(error);
},function(progress){
console.log(progress);
});
defer.notify('in progress');//控制臺打印in progress
defer.resolve('resolve'); //控制臺打印resolve
defer.reject('reject'); //沒有輸出。promise的狀態只能改變一次
~~~
## [](https://github.com/alsotang/node-lessons/tree/master/lesson17#promise的傳遞)promise的傳遞
then方法會返回一個promise,在下面這個例子中,我們用outputPromise指向then返回的promise。
~~~
var outputPromise = getInputPromise().then(function (fulfilled) {
}, function (rejected) {
});
~~~
現在outputPromise就變成了受?`function(fulfilled)`?或者?`function(rejected)`控制狀態的promise了。怎么理解這句話呢?
* 當function(fulfilled)或者function(rejected)返回一個值,比如一個字符串,數組,對象等等,那么outputPromise的狀態就會變成fulfilled。
在下面這個例子中,我們可以看到,當我們把inputPromise的狀態通過defer.resovle()變成fulfilled時,控制臺輸出fulfilled.
當我們把inputPromise的狀態通過defer.reject()變成rejected,控制臺輸出rejected
~~~
var Q = require('q');
var defer = Q.defer();
/**
* 通過defer獲得promise
* @private
*/
function getInputPromise() {
return defer.promise;
}
/**
* 當inputPromise狀態由未完成變成fulfil時,調用function(fulfilled)
* 當inputPromise狀態由未完成變成rejected時,調用function(rejected)
* 將then返回的promise賦給outputPromise
* function(fulfilled) 和 function(rejected) 通過返回字符串將outputPromise的狀態由
* 未完成改變為fulfilled
* @private
*/
var outputPromise = getInputPromise().then(function(fulfilled){
return 'fulfilled';
},function(rejected){
return 'rejected';
});
/**
* 當outputPromise狀態由未完成變成fulfil時,調用function(fulfilled),控制臺打印'fulfilled: fulfilled'。
* 當outputPromise狀態由未完成變成rejected, 調用function(rejected), 控制臺打印'fulfilled: rejected'。
*/
outputPromise.then(function(fulfilled){
console.log('fulfilled: ' + fulfilled);
},function(rejected){
console.log('rejected: ' + rejected);
});
/**
* 將inputPromise的狀態由未完成變成rejected
*/
defer.reject(); //輸出 fulfilled: rejected
/**
* 將inputPromise的狀態由未完成變成fulfilled
*/
//defer.resolve(); //輸出 fulfilled: fulfilled
~~~
* 當function(fulfilled)或者function(rejected)拋出異常時,那么outputPromise的狀態就會變成rejected
~~~
var Q = require('q');
var fs = require('fs');
var defer = Q.defer();
/**
* 通過defer獲得promise
* @private
*/
function getInputPromise() {
return defer.promise;
}
/**
* 當inputPromise狀態由未完成變成fulfil時,調用function(fulfilled)
* 當inputPromise狀態由未完成變成rejected時,調用function(rejected)
* 將then返回的promise賦給outputPromise
* function(fulfilled) 和 function(rejected) 通過拋出異常將outputPromise的狀態由
* 未完成改變為reject
* @private
*/
var outputPromise = getInputPromise().then(function(fulfilled){
throw new Error('fulfilled');
},function(rejected){
throw new Error('rejected');
});
/**
* 當outputPromise狀態由未完成變成fulfil時,調用function(fulfilled)。
* 當outputPromise狀態由未完成變成rejected, 調用function(rejected)。
*/
outputPromise.then(function(fulfilled){
console.log('fulfilled: ' + fulfilled);
},function(rejected){
console.log('rejected: ' + rejected);
});
/**
* 將inputPromise的狀態由未完成變成rejected
*/
defer.reject(); //控制臺打印 rejected [Error:rejected]
/**
* 將inputPromise的狀態由未完成變成fulfilled
*/
//defer.resolve(); //控制臺打印 rejected [Error:fulfilled]
~~~
* 當function(fulfilled)或者function(rejected)返回一個promise時,outputPromise就會成為這個新的promise.
這樣做有什么意義呢? 主要在于聚合結果(Q.all),管理延時,異常恢復等等
比如說我們想要讀取一個文件的內容,然后把這些內容打印出來。可能會寫出這樣的代碼:
~~~
//錯誤的寫法
var outputPromise = getInputPromise().then(function(fulfilled){
fs.readFile('test.txt','utf8',function(err,data){
return data;
});
});
~~~
然而這樣寫是錯誤的,因為function(fulfilled)并沒有返回任何值。需要下面的方式:
~~~
var Q = require('q');
var fs = require('fs');
var defer = Q.defer();
/**
* 通過defer獲得promise
* @private
*/
function getInputPromise() {
return defer.promise;
}
/**
* 當inputPromise狀態由未完成變成fulfil時,調用function(fulfilled)
* 當inputPromise狀態由未完成變成rejected時,調用function(rejected)
* 將then返回的promise賦給outputPromise
* function(fulfilled)將新的promise賦給outputPromise
* 未完成改變為reject
* @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');
});
/**
* 當outputPromise狀態由未完成變成fulfil時,調用function(fulfilled),控制臺打印test.txt文件內容。
*
*/
outputPromise.then(function(fulfilled){
console.log(fulfilled);
},function(rejected){
console.log(rejected);
});
/**
* 將inputPromise的狀態由未完成變成rejected
*/
//defer.reject();
/**
* 將inputPromise的狀態由未完成變成fulfilled
*/
defer.resolve(); //控制臺打印出 test.txt 的內容
~~~
## [](https://github.com/alsotang/node-lessons/tree/master/lesson17#方法傳遞)方法傳遞
方法傳遞有些類似于Java中的try和catch。當一個異常沒有響應的捕獲時,這個異常會接著往下傳遞。
方法傳遞的含義是當一個狀態沒有響應的回調函數,就會沿著then往下找。
* 沒有提供function(rejected)
~~~
var outputPromise = getInputPromise().then(function(fulfilled){})
~~~
如果inputPromise的狀態由未完成變成rejected, 此時對rejected的處理會由outputPromise來完成。
~~~
var Q = require('q');
var fs = require('fs');
var defer = Q.defer();
/**
* 通過defer獲得promise
* @private
*/
function getInputPromise() {
return defer.promise;
}
/**
* 當inputPromise狀態由未完成變成fulfil時,調用function(fulfilled)
* 當inputPromise狀態由未完成變成rejected時,這個rejected會傳向outputPromise
*/
var outputPromise = getInputPromise().then(function(fulfilled){
return 'fulfilled'
});
outputPromise.then(function(fulfilled){
console.log('fulfilled: ' + fulfilled);
},function(rejected){
console.log('rejected: ' + rejected);
});
/**
* 將inputPromise的狀態由未完成變成rejected
*/
defer.reject('inputpromise rejected'); //控制臺打印rejected: inputpromise rejected
/**
* 將inputPromise的狀態由未完成變成fulfilled
*/
//defer.resolve();
~~~
* 沒有提供function(fulfilled)
~~~
var outputPromise = getInputPromise().then(null,function(rejected){})
~~~
如果inputPromise的狀態由未完成變成fulfilled, 此時對fulfil的處理會由outputPromise來完成。
~~~
var Q = require('q');
var fs = require('fs');
var defer = Q.defer();
/**
* 通過defer獲得promise
* @private
*/
function getInputPromise() {
return defer.promise;
}
/**
* 當inputPromise狀態由未完成變成fulfil時,傳遞給outputPromise
* 當inputPromise狀態由未完成變成rejected時,調用function(rejected)
* function(fulfilled)將新的promise賦給outputPromise
* 未完成改變為reject
* @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的狀態由未完成變成rejected
*/
//defer.reject('inputpromise rejected');
/**
* 將inputPromise的狀態由未完成變成fulfilled
*/
defer.resolve('inputpromise fulfilled'); //控制臺打印fulfilled: inputpromise fulfilled
~~~
* 可以使用fail(function(error))來專門針對錯誤處理,而不是使用then(null,function(error))
~~~
var outputPromise = getInputPromise().fail(function(error){})
~~~
看這個例子
~~~
var Q = require('q');
var fs = require('fs');
var defer = Q.defer();
/**
* 通過defer獲得promise
* @private
*/
function getInputPromise() {
return defer.promise;
}
/**
* 當inputPromise狀態由未完成變成fulfil時,調用then(function(fulfilled))
* 當inputPromise狀態由未完成變成rejected時,調用fail(function(error))
* function(fulfilled)將新的promise賦給outputPromise
* 未完成改變為reject
* @private
*/
var outputPromise = getInputPromise().then(function(fulfilled){
return fulfilled;
}).fail(function(error){
console.log('fail: ' + error);
});
/**
* 將inputPromise的狀態由未完成變成rejected
*/
defer.reject('inputpromise rejected');//控制臺打印fail: inputpromise rejected
/**
* 將inputPromise的狀態由未完成變成fulfilled
*/
//defer.resolve('inputpromise fulfilled');
~~~
* 可以使用progress(function(progress))來專門針對進度信息進行處理,而不是使用?`then(function(success){},function(error){},function(progress){})`
~~~
var Q = require('q');
var defer = Q.defer();
/**
* 獲取初始promise
* @private
*/
function getInitialPromise() {
return defer.promise;
}
/**
* 為promise設置progress信息處理函數
*/
var outputPromise = getInitialPromise().then(function(success){
}).progress(function(progress){
console.log(progress);
});
defer.notify(1);
defer.notify(2); //控制臺打印1,2
~~~
## [](https://github.com/alsotang/node-lessons/tree/master/lesson17#promise鏈)promise鏈
promise鏈提供了一種讓函數順序執行的方法。
函數順序執行是很重要的一個功能。比如知道用戶名,需要根據用戶名從數據庫中找到相應的用戶,然后將用戶信息傳給下一個函數進行處理。
~~~
var Q = require('q');
var defer = Q.defer();
//一個模擬數據庫
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');
~~~
我們通過兩個then達到讓函數順序執行的目的。
then的數量其實是沒有限制的。當然,then的數量過多,要手動把他們鏈接起來是很麻煩的。比如
~~~
foo(initialVal).then(bar).then(baz).then(qux)
~~~
這時我們需要用代碼來動態制造promise鏈
~~~
var funcs = [foo,bar,baz,qux]
var result = Q(initialVal)
funcs.forEach(function(func){
result = result.then(func)
})
return result
~~~
當然,我們可以再簡潔一點
~~~
var funcs = [foo,bar,baz,qux]
funcs.reduce(function(pre,current),Q(initialVal){
return pre.then(current)
})
~~~
看一個具體的例子
~~~
function foo(result) {
console.log(result);
return result+result;
}
//手動鏈接
Q('hello').then(foo).then(foo).then(foo); //控制臺輸出: hello
// hellohello
// hellohellohello
//動態鏈接
var funcs = [foo,foo,foo];
var result = Q('hello');
funcs.forEach(function(func){
result = result.then(func);
});
//精簡后的動態鏈接
funcs.reduce(function(prev,current){
return prev.then(current);
},Q('hello'));
~~~
對于promise鏈,最重要的是需要理解為什么這個鏈能夠順序執行。如果能夠理解這點,那么以后自己寫promise鏈可以說是輕車熟路啊。
## [](https://github.com/alsotang/node-lessons/tree/master/lesson17#promise組合)promise組合
回到我們一開始讀取文件內容的例子。如果現在讓我們把它改寫成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;
}
}
//手動鏈接
printFileContent('sample01.txt')()
.then(printFileContent('sample02.txt'))
.then(printFileContent('sample03.txt'))
.then(printFileContent('sample04.txt')); //控制臺順序打印sample01到sample04的內容
~~~
很有成就感是不是。然而如果仔細分析,我們會發現為什么要他們順序執行呢,如果他們能夠并行執行不是更好嗎? 我們只需要在他們都執行完成之后,得到他們的執行結果就可以了。
我們可以通過Q.all([promise1,promise2...])將多個promise組合成一個promise返回。 注意:
1. 當all里面所有的promise都fulfil時,Q.all返回的promise狀態變成fulfil
2. 當任意一個promise被reject時,Q.all返回的promise狀態立即變成reject
我們來把上面讀取文件內容的例子改成并行執行吧
~~~
var Q = require('q');
var fs = require('fs');
/**
*讀取文件內容
*@private
*/
function printFileContent(fileName) {
//Todo: 這段代碼不夠簡潔。可以使用Q.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);
}); //控制臺打印各個文件內容 順序不一定
~~~
現在知道Q.all會在任意一個promise進入reject狀態后立即進入reject狀態。如果我們需要等到所有的promise都發生狀態后(有的fulfil, 有的reject),再轉換Q.all的狀態, 這時我們可以使用Q.allSettled
~~~
var Q = require('q'),
fs = require('fs');
/**
*讀取文件內容
*@private
*/
function printFileContent(fileName) {
//Todo: 這段代碼不夠簡潔。可以使用Q.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);
}
);
});
~~~
## [](https://github.com/alsotang/node-lessons/tree/master/lesson17#結束promise鏈)結束promise鏈
通常,對于一個promise鏈,有兩種結束的方式。第一種方式是返回最后一個promise
如?`return foo().then(bar);`
第二種方式就是通過done來結束promise鏈
如?`foo().then(bar).done()`
為什么需要通過done來結束一個promise鏈呢? 如果在我們的鏈中有錯誤沒有被處理,那么在一個正確結束的promise鏈中,這個沒被處理的錯誤會通過異常拋出。
~~~
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()結束的promise鏈
*由于getPromse('2',2000,'opt')返回rejected, getPromise('3',1000)就沒有執行
*然后這個異常并沒有任何提醒,是一個潛在的bug
*/
getPromise('1',3000)
.then(function(){return getPromise('2',2000,'opt')})
.then(function(){return getPromise('3',1000)});
/**
*用done()結束的promise鏈
*有異常拋出
*/
getPromise('1',3000)
.then(function(){return getPromise('2',2000,'opt')})
.then(function(){return getPromise('3',1000)})
.done();
~~~
## [](https://github.com/alsotang/node-lessons/tree/master/lesson17#結束語)結束語
當你理解完上面所有的知識點時,你就會正確高效的使用promise了。本節只是講了promise的原理和幾個基本的API,不過你掌握了這些之后,再去看q的文檔,應該很容易就能理解各個api的意圖。
- 關于
- Lesson 0: 《搭建 Node.js 開發環境》
- Lesson 1: 《一個最簡單的 express 應用》
- Lesson 2: 《學習使用外部模塊》
- Lesson 3: 《使用 superagent 與 cheerio 完成簡單爬蟲》
- Lesson 4: 《使用 eventproxy 控制并發》
- Lesson 5: 《使用 async 控制并發》
- Lesson 6: 《測試用例:mocha,should,istanbul》
- Lesson 7: 《瀏覽器端測試:mocha,chai,phantomjs》
- Lesson 8: 《測試用例:supertest》
- Lesson 9: 《正則表達式》
- Lesson 10: 《benchmark 怎么寫》
- Lesson 11: 《作用域與閉包:this,var,(function () {})》
- Lesson 12: 《線上部署:heroku》
- Lesson 13: 《持續集成平臺:travis》
- Lesson 14: 《js 中的那些最佳實踐》
- Lesson 15: 《Mongodb 與 Mongoose 的使用》
- Lesson 16: 《cookie 與 session》
- Lesson 17: 《使用 promise 替代回調函數》