Promise是JavaScript中的一種異步編程范式, 一個Promise對象表示一個即將完成但還未完成的操作。 鑒于JavaScript中異步和回調的編程風格, Promise模式可以有效地避免『Callback Hell』。
Promise 最初有q和bluebird等實現,在ES2015(ES6)提出后Promise已經進入標準,Node.js已經開始支持ES6的很多特性,包括Promise。
初始化
傳入一個回調函數即可初始化一個Promise對象padmin:
var padmin = new Promise(function(resolve, reject){
user.find({role: 'admin'}, function(err, admins){
if(err) reject(err);
else resolve(admins);
});
});
除此之外,ES6還給出4種常用的初始化方式,下列方法均返回一個Promise對象:
方法
說明
Promise.all(iterable)
當iterable(比如數組)中所有Promise都resolve時,該Promise resolve;iterable中任何一個被reject,則該Promise被reject
Promise.race(iterable)
當iterable中任意一個Promise被resolve或reject,該Promise都會相應地結束
Promise.reject(err)
直接返回一個被reject的Promise對象
Promise.reject(value)
直接返回一個被resolve的Promise對象
Promise對象
Promise對象padmin擁有兩個主要方法:
方法
說明
Promise.prototype.catch(onRejected)
當一個Promise被reject時調用onRejected
Promise.prototype.then(onFulfilled, onRejected)
當一個Promise被resolve時調用onFulfilled,被reject時調用onRejected
上述兩個方法均返回一個Promise,這意味著.then和.catch可以鏈式書寫。例如:
padmin
.then(function(admins){
doSthWith(admins);
})
.catch(function(err){
console.error(err);
});
統一錯誤處理
在任何一個then()回調中拋出的錯誤都會被后面的catch()所截獲,以此可以做統一的錯誤處理:
padmin
.then(function(admins){
if(admins === null) throw new Error('query admin error');
return admins.length;
})
.then(function(length){
if(length === 0) throw new Error('empty admin list');
console.log(length + ' admins in total.');
})
.catch(function(err){
console.error(err);
});
Promisify
Node.js的內置庫以及大量的NPM工具都采用『Error-First Callback』風格,例如:
fs.readFile('foo.txt', function(err, content){
if(err) console.error(err);
else console.log(content);
});
在Promise風格的代碼中,通常會需要readFile返回一個Promise對象,于是常常會這樣包裝該API:
var readFileAsync = function(path){
return new Promise(function(resolve, reject){
fs.readFile(path, function(err, content){
if(err) reject(err);
else resolve(content);
});
});
}
readFileAsync('foo.txt')
.then(function(content){
console.log(content):
})
.catch(function(err){
console.error(err);
});
然而我們需要包裝fs模塊下的所有API 🙁 bluebird為此提供了有用的方法promisifyAll():
var fs = require("fs");
// 為fs的所有方法創建一個Promise包裝,命名為xxxAsync
Promise.promisifyAll(fs);
fs.readFileAsync("foo.txt").then(...).catch(...);
當然也可以只包裝一個函數:
var readFile = Promise.promisify(require("fs").readFile);
readFile("foo.txt").then(...).catch(...);
fromCallback
現在我們有了.promisify來把一個『Error-First Callback』風格的API包裝為Promise風格。 在某些特定情形下,可能每次使用都需要先進行promisify,比如使用后即被銷毀的臨時對象。 例如從HTTP請求構造的req對象每次請求都是新的:
function(req, res, next){
User.find({name: req.body.name})
.then(function(user) {
var login = Promise.promisify(req.login);
return login.call(req, user);
})
.catch(next);
}
這時可以用Promise.fromCallback方法,直接由『Error-First Callback』調用生成Promise對象,而不需要生成Promise風格的方法。
function(req, res, next){
User.find({name: req.body.name})
.then(function(user) {
return BPromise.fromCallback(cb => req.login(user, cb));
})
.catch(next);
}
Mongoose Promisify
mongoose是MongoDB在JavaScript下的適配器(類似ORM),提供了模型驗證、數據轉換、業務邏輯鉤子、查詢鉤子等對象建模工具。 mongoose有些API(如.exec())會返回內置的Promise,我們可以用一個更強的Promise來替代它:
var BPromise = require('bluebird');
mongoose.Promise = BPromise;
除exec(), execPopulate()系列函數外,mongoose多數API都是回調風格的,通常需要用Bluebird將其Promisify。 這些Mongoose API主要包括下列三類:
Model. Eg: User.findAsync(), User.findByIdAsync(), User.removeAsync(), User.updateAsync()
Model.prototype. Eg: user.saveAsync(), user.removeAsync()
Query.prototype. Eg: User.find().sortAsync(), User.find().populateAsync()
BPromise.promisifyAll(mongoose.Model);
BPromise.promisifyAll(mongoose.Model.prototype);
BPromise.promisifyAll(mongoose.Query.prototype);
這些Promise化的代碼最好在代碼載入時執行,但不要早于mongoose插件。否則這些插件就不會被Promise化了。
Promise化之后的mongoose用起來是這樣的:
var UserSchema = mongoose.Schema({
name: String,
phone: String
});
var User = mongoose.model('User', UserSchema);
User.findAsync()
.then(users => console.log(users));
.catch(e => console.error(e));
某些mongoose插件可能需要在Promisify腳本之后執行較為方便。這時我們需要將受影響的模型再次Promise化:
var UserSchema = mongoose.Schema({...});
UserSchema.plugin(require('passport-local-mongoose'), {
usernameField: 'phone'
});
var User = mongoose.model('User', UserSchema);
BPromise.promisifyAll(User);
文章出處