是时候熟悉一下 Promise 异步编程范式了!
Promise 是 JavaScript 中的一种异步编程范式, 一个 Promise 对象表示一个即将完成但还未完成的操作。 鉴于 JavaScript 中异步和 回调 的编程风格, Promise 模式可以有效地避免『Callback Hell』。
Promise 最初由 Q, jQuery 1.5, RSVP.js, bluebird 等实现, 现有 Promises/A+ 社区标准可以参考:https://promisesaplus.com/,在 纳入 ES6 后浏览器和 Node.js 都相继给出了实现。
初始化
传入一个回调函数即可初始化一个 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);
本文采用 知识共享署名 4.0 国际许可协议(CC-BY 4.0)进行许可,转载注明来源即可: https://harttle.land/2016/08/10/promise.html。如有疏漏、谬误、侵权请通过评论或 邮件 指出。