初识Promise
对于mongoBD的异步控制,在项目实践中使用了async库来控制复杂的异步回调函数处理。很有幸参加i5ting老师的《Node.js最新技术栈之Promise篇》微课堂。老师主要从promise的起源、实现以及实践和展望几点简述使用Promise的心得。有所收获,现在总结如下,其中例子为老师提供。
- Why
首先回调和异步在nodejs中十分常见,因为nodejs的最大的优点:高并发,适合适合I/O密集型应用,就是通过异步处理实现。但回调函数的嵌套会导致意大利面条式的代码[1]。比如下面这种一个分为4步的任务:每步任务都需要上一步的返回才能继续执行。
step1(function (value1) {
// Do something with value1
step2(value1, function(value2) {
// Do something with value2
step3(value2, function(value3) {
// Do something with value3
step4(value3, function(value4) {
// Do something with value4
});
});
});
});
promise[2]就是来解决js中这种频繁回调导致的问题,promise是可以通过像jQuery的链式调用这样来控制回调函数,链式写法: $('#tab').eq($(this).index()).show().siblings().hide();
。每个方法都返回this对象传递给下一个方法。比如下面这个例子:
var obj = {
step1:function(){
console.log('a');
return this;
},
step2:function(){
console.log('b');
return this;
},
step3:function(){
console.log('c');
return this;
},
step4:function(){
console.log('d');
return this;
}
}
console.log('——\n');
obj.step1().step2().step3();
console.log('——\n');
obj.step4().step2().step1();
输出结果abc以及dba。这就是promise,而promise的意思是还未获得的值,只是作为一个占位符给下一个方法。正如promise是还未获得的值,deferred表示未尚未完成的任务步骤,而deferred可以被解决(resolved),也可以被拒绝(rejected)。
回到上面的回调函数:step1().step2().step3().step4(),每个操作都是独立的,可组装。但如果需要上一步的输出为下一步的输入,比如linux的 pipe:ps -ef|grep node|awk ‘{print $2}’|xargs kill -9,或者需要捕获异常,控制流程呢?这就是promise规范的起因。
- What
官方给出的promise/a+规范的定义为:Promise表示一个异步操作的最终结果。与Promise最主要的交互方法是通过将函数传入它的then方法从而获取得Promise最终的值或Promise最终最拒绝(reject)的原因。还是上面那个例子:异步操作的最终结果: return step1().step2().step3().step4()。如果在promise里,就是这样:
return step1().then(step2).then(step3).then(step4).catch(function(err){
// do something when err
});
定义中交互方式的几点都都对应了,step2传入then方法来获取step1的返回值,catch来捕获error,catch后的回调函数就是处理error。有一个reject,还对应一个resolve方法。
- reject 是拒绝,跳转到catch error
- resolve 是解决,下一步,即跳转到下一个promise操作
比如下方这个例子,step1失败就不运行step2,调到下一个promise操作,执行step3。
function step1(){
if(false){
return step3().then(step4).catch(function(err){
// do something when err
});
}
}
return step1().then(step2).then(step3).then(step4).catch(function(err){
// do something when err
});
再来看看promise的几个要点。
- promise 是一个包含了兼容promise规范then方法的对象或函数。我们可以这样理解,每一个promise只要返回的可以then的都可以。就像上面举例返回的this一样,只要每一个都返回this,她就可以无限的链式下去。
- thenable 是一个包含了then方法的对象或函数。
- value 是任何Javascript值。 (包括 undefined, thenable, promise等).
- exception 是由throw表达式抛出来的值。当流程出现异常的适合,把异常抛出来,由catch err处理。
- reason 是一个用于描述Promise被拒绝原因的值。
最后总结一下promise,1) 每个操作都返回一样的promise对象,保证链式操作;2) 每个链式都通过then方法;3) 每个操作内部允许犯错,出了错误,统一由catch error处理;4) 操作内部,也可以是一个操作链,通过reject或resolve再生成流程。还有一些复杂的规范细节[3]需要在实践中才知道意义。
nodejs里的promise实现主要有一下几种,最近在使用async来控制回调流程。
- bluebird 拥有不错的性能,后面继续讲
- q Angularjs的$q对象是q的精简版
- then teambition作品
- when Promises/A+和when()的实现
- async 最简单的)
- eventproxy 朴灵作品,使用event来
其他语言实现,详见 https://promisesaplus.com/implementations
- how
下面进入到如何实现promise的内容,一个很初步的原型如下代码:
//promise原型对象
var Promise = function () {
};
//类型判断
var isPromise = function (value) {
return value instanceof Promise;
};
//给defer对象返回resolve和promise
var defer = function () {
var pending = [], value;
//声明
var promise = new Promise();
//增加then方法
promise.then = function (callback) {
if (pending) {
pending.push(callback);
} else {
callback(value);
}
};
//返回
//value传参
return {
resolve: function (_value) {
if (pending) {
value = _value;
for (var i = 0, ii = pending.length; i < ii; i++) {
var callback = pending[i];
callback(value);
}
pending = undefined;
}
},
promise: promise
};
};
我们还可以看看q,angularjs的$q就是它的精简版,它把q的7个版本是如何实现的都详细记录4了。
了解到基本的实现就需要考虑在项目中的实践了。i5ting的经验是公司项目使用bluebird,而小项目使用async。几种实现的benchmark。这样一看bludbird确实性能比较好。比如并行执行任务的比较如下:
bluebird特性
- 速度最快
- api和文档完善,(对各个库支持都不错)
- 支持generator等未来发展趋势
- github活跃
- 还有让人眼前一亮的功能点
下面举例的是bluebird的promisify,promisify原理就是你给他传一个对象或者prototype,它去遍历,给他们加上async方法,此方法返回promise对象,你就可以像上面那样使用promise的特点了,但这样得谨防对象过大,导致内存问题。熟悉fs的API的都知道,fs有fs.readFile()
、fs.readFileSync()
方法,但没有fs.readFileAsync()
。实际上fs.readFileAsync()
是bluebird加上去的,使用promisifyAll()
来实现这个功能。
var fs = Promise.promisifyAll(require(“fs”));
按照MVC的流程,首先定义模型:
var mongoose = require(‘mongoose’);
var Schema = mongoose.Schema;
var Promise = require(“bluebird”);
UserSchema = new Schema({
username: String,
password: String,
created_at: {
type: Date,
"default": Date.now
}
});
var User = mongoose.model(‘User’, UserSchema);
Promise.promisifyAll(User);
Promise.promisifyAll(User.prototype);
然后就是业务逻辑,直接调用findAsync的:
User.findAsync({username: ‘username’}).then(function(data) {
…
}).catch(function(err) {
…
});
可以优化的地方在于,直接mongoose的static和method上扩展,不暴露太多细节。面对promise,保证每个操作都是函数,使得流程可以组装。比如下列这种情况,Team可以添加新用户:
//用函数封装操作
function find_user(){
return User.findByAsync(user_id);
}
//直接调用mongoose方法
Team.updateByIdAync(a, b, c).then(find_user).catch(error);
扩展一下数据库driver这边,ioredis也支持的很好,能将与redis的交互建立在promise的控制下,而mongoose支持promise也越发证明这个工具与原生driver的不同。
- es6的实现 generator/yield
generator是es6新添加的功能,generator(生成器函数):不会立即执行,需要再执行迭代操作(.next),yield抛出断点等待next的调用。使用这种方法的库有co,其使用例子如下:
co(function* (){
yield Something.save();
}).then(function() {
// success
})
.catch(function(err) {
//error handling
});
而yield的并行直接则是这样写: yield[fun1(), fun2()];
es7的实现 async/await es7则通过async关键词来执行异步操作,使用await执行异步操作的例子如下,可以看到这与async库之间的区别:
async function save(fun1){ try{ await fun1.save(); } }
最后再总结一次promise的要点:
- 异步操作的最终结果,尽可能每一个异步操作都是独立操作单元
- 与Promise最主要的交互方法是通过将函数传入它的then方法(thenable)
- 捕获异常catch error
- 根据reject和resolve重塑流程
而generator是一种新的定义方式,定义操作单元,尤其在迭代器的情况,搭配yield来执行,可读性上差了很多,好处是真的解耦了。co是一个中间产品,可以说是给generator增加了promise实现,可读性和易用性是愿意好于generator + yield的。最后我们看看async,它实际上是通过async这个关键词,定义的函数就可以返回promise对象,可以说async就是能返回promise对象的generator,yield关键词以及被generator绑架了,那它就换个名字,叫await。
其实从这段历史来看,反复就是promise上的折腾,只是加了generator这个别名,只是async是能返回promise的generator。
这次学习大致就是这么多内容,其细节还需要在项目上实践,也确定了使用bb作为以后express的异步控制库。
参考:
- Spaghetti Code:意大利面条代码的定义
- Promise Github:源码
- Promise+官网:规范细节
- q的7个版本详细记录:设计思路
- Promise & Deferred objects in JavaScript Pt.1: Theory and Semantics.:promise和deferred这些概念的理解