第五課講述了如何使用 async 來控制并發(fā)。async 的本質(zhì)是一個(gè)流程控制。其實(shí)在異步編程中,還有一個(gè)更為經(jīng)典的模型,叫做 Promise/Deferred 模型。
本節(jié)我們就來學(xué)習(xí)這個(gè)模型的代表實(shí)現(xiàn):q
首先,我們思考一個(gè)典型的異步編程模型,考慮這樣一個(gè)題目:讀取一個(gè)文件,在控制臺輸出這個(gè)文件內(nèi)容。
var fs = require('fs');
fs.readFile('sample.txt', 'utf8', function (err, data) {
console.log(data);
});
看起來很簡單,再進(jìn)一步: 讀取兩個(gè)文件,在控制臺輸出這兩個(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來改善它。
先學(xué)習(xí)promise的基本概念。
promise有一個(gè)then方法,then方法可以接受3個(gè)函數(shù)作為參數(shù)。前兩個(gè)函數(shù)對應(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');//控制臺打印in progress
defer.resolve('resolve'); //控制臺打印resolve
defer.reject('reject'); //沒有輸出。promise的狀態(tài)只能改變一次
then方法會返回一個(gè)promise,在下面這個(gè)例子中,我們用outputPromise指向then返回的promise。
var outputPromise = getInputPromise().then(function (fulfilled) {
}, function (rejected) {
});
現(xiàn)在outputPromise就變成了受 function(fulfilled)
或者 function(rejected)
控制狀態(tài)的promise了。怎么理解這句話呢?
在下面這個(gè)例子中,我們可以看到,當(dāng)我們把inputPromise的狀態(tài)通過defer.resovle()變成fulfilled時(shí),控制臺輸出fulfilled.
當(dāng)我們把inputPromise的狀態(tài)通過defer.reject()變成rejected,控制臺輸出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),控制臺打印'fulfilled: fulfilled'。
* 當(dāng)outputPromise狀態(tài)由未完成變成rejected, 調(diào)用function(rejected), 控制臺打印'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
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(); //控制臺打印 rejected [Error:rejected]
/**
* 將inputPromise的狀態(tài)由未完成變成fulfilled
*/
//defer.resolve(); //控制臺打印 rejected [Error:fulfilled]
這樣做有什么意義呢? 主要在于聚合結(jié)果(Q.all),管理延時(shí),異?;謴?fù)等等
比如說我們想要讀取一個(gè)文件的內(nèi)容,然后把這些內(nèi)容打印出來。可能會寫出這樣的代碼:
//錯(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),控制臺打印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(); //控制臺打印出 test.txt 的內(nèi)容
方法傳遞有些類似于Java中的try和catch。當(dāng)一個(gè)異常沒有響應(yīng)的捕獲時(shí),這個(gè)異常會接著往下傳遞。
方法傳遞的含義是當(dāng)一個(gè)狀態(tài)沒有響應(yīng)的回調(diào)函數(shù),就會沿著then往下找。
var outputPromise = getInputPromise().then(function(fulfilled){})
如果inputPromise的狀態(tài)由未完成變成rejected, 此時(shí)對rejected的處理會由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會傳向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'); //控制臺打印rejected: inputpromise rejected
/**
* 將inputPromise的狀態(tài)由未完成變成fulfilled
*/
//defer.resolve();
var outputPromise = getInputPromise().then(null,function(rejected){})
如果inputPromise的狀態(tài)由未完成變成fulfilled, 此時(shí)對fulfil的處理會由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'); //控制臺打印fulfilled: inputpromise fulfilled
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');//控制臺打印fail: inputpromise rejected
/**
* 將inputPromise的狀態(tài)由未完成變成fulfilled
*/
//defer.resolve('inputpromise fulfilled');
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); //控制臺打印1,2
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); //控制臺輸出: 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'));
對于promise鏈,最重要的是需要理解為什么這個(gè)鏈能夠順序執(zhí)行。如果能夠理解這點(diǎn),那么以后自己寫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')); //控制臺順序打印sample01到sample04的內(nèi)容
很有成就感是不是。然而如果仔細(xì)分析,我們會發(fā)現(xiàn)為什么要他們順序執(zhí)行呢,如果他們能夠并行執(zhí)行不是更好嗎? 我們只需要在他們都執(zhí)行完成之后,得到他們的執(zhí)行結(jié)果就可以了。
我們可以通過Q.all([promise1,promise2...])將多個(gè)promise組合成一個(gè)promise返回。 注意:
我們來把上面讀取文件內(nèi)容的例子改成并行執(zhí)行吧
var Q = require('q');
var fs = require('fs');
/**
*讀取文件內(nèi)容
*@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);
}); //控制臺打印各個(gè)文件內(nèi)容 順序不一定
現(xiàn)在知道Q.all會在任意一個(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: 這段代碼不夠簡潔。可以使用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);
}
);
});
通常,對于一個(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ò)誤會通過異常拋出。
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();
當(dāng)你理解完上面所有的知識點(diǎn)時(shí),你就會正確高效的使用promise了。本節(jié)只是講了promise的原理和幾個(gè)基本的API,不過你掌握了這些之后,再去看q的文檔,應(yīng)該很容易就能理解各個(gè)api的意圖。
更多建議: