该文章阅读需要5分钟,更多文章请点击本人博客halu886
异步编程解决方案
Promise/Deferred模式
使用事件的方式时,执行流程都需要预先设定好,并且包括相关分支的逻辑,这是由于发布/订阅模式决定的。
$.get('/api',{
success: onSuccess,
error: onError,
complete: onComplete
})
上面的方式,必须要提前设计好如何处理目标。但是如果使用Promise/Deferred模式,则可以实现先执行异步调用,延迟传递处理。
Promise/Deferred模式被JQuery1.5中广为人知。Ajax经过重写后,调用Ajax可以通过如下调用。
$.get('/api')
.success(onSuccess)
.error(onError)
.complete(onComplete)
这个方式比预先传入一个对象的方式更加灵活,并且不调用success()
,error()
,complete()
,请求也会照常调用。而且原先一个事件只能处理一个回调,但是如下所示,可以对事件加入任意多个业务逻辑。
$.get('/api/)
.success(onSuccess1)
.success(onSuccess2);
Promise/Deferred在2009年被抽象Promise/A,Promise/B,Promise/D 这样典型的Promise/Deferred 异步模型作为草案发布在CommonJs规范中。使得异步调用更加优雅。
异步的广度使得回调的增加,一旦回调增加,编程体验将会变的很不愉快,但是Promise/Deferred 模式在一定程度上缓解了这个问题。
Promise/A
Promise/A提议对单个异步操作做出如下定义。
- Promise只会存在以下三个状态:未完成态,完成态和失败态。
- Promise的状态只会出现从未完成态转向完成态或失败态。不能逆转,完成态和失败态不能相互转化。
- Promise的状态一旦改变,将不能改变。
Promise/A 模式的API的定义比较简单,Promise 对象存在then()方法即可。不过then()方法有如下要求。
- 接受完成态或错误态的回调方法,当操作完成时或者错误时,调用相应的方法。
- 可选的支持progress事件回调作为第三个方法。
then()
只接受function对象,其他对象将会忽略。then()
返回Promise,支持链式调用。
定义如下
then(fulfilledHandler,errorHandler,progressHandler)
var Promise = function(){
EventEmitter.call(this);
}
util.inherits(Promise,EventEmitter);
Promise.prototype.then = function(fulfillHandler,errorHandler,progressHandler){
if(typeof fulfilledHandler === 'function'){
this.once('success',fulfilledHandler);
}
if(typeof errorHandler === 'function'){
this.once('error',errorHandler);
}
if(typeof progressHandler === 'function'){
this.on('progress',progressHandler);
}
return this;
};
then()方法所做的工作就是存放这些回调函数。为了完成整个流程,还需要触发这些回调,我们把这些对象称为Deferred,即延时对象。
var Deferred = function(){
this.state = 'nufulfilled';
this.promise = new Promise();
}
Deferred.prototype.resolve = function(obj){
this.state= 'fulfilled';
this.promise.emit('success',obj);
}
Deferred.prototype.reject = function(err){
this.state = 'failed';
this.promise.emit('error',err);
}
Deferred.prototype.progress = function(data){
this.promise.emit('progress',data);
}
这里的状态和方法的关系如图所示:
我们可以根据典型的响应对象进行封装。
res.setEncoding('utf8');
res.on('data',function(chunk){
console.log('BODY:' + chunk);
});
res.on('end',function(){
// Done
})
res.on('error',function(err){
// Error
})
转换如下
res.then(function(){
//Done
},function(err){
//Error
},function(chunk){
console.log('BODY:' + chunk);
})
改造代码如下
var promisify = function(res){
var deferred = new Deferred();
var result = '';
res.on('data',function(chunk){
result += chunk;
deferred.progress(chunk);
});
res.on('end',function(){
deferred.resolve(result);
});
res.on('error',function(err){
deferred.reject(err);
});
return deferred.promise;
}
如此便得到了简单的结果,返回promise 的目的是为了不让外部使用reject 和resolve。更改内部状态由定义者处理。
promisify(res).then(function(){
// Done
},function (err){
// Error
},function (chunk){
// progress
console.log('BODY:' + chunk);
})
Deferred主要是用于处理内部状态变化,维护异步模型的状态。Promise用于外部,通过then()方法暴露给外部添加自定义方法。
Promise 和Deferred的整体关系如图所示。
与事件订阅/发布模式相比,Promise/Deferred 模式的API接口和抽象模型变得更加灵活和简单。将业务中不变的封装在Deferred中,变化的封装在Promise中,在不同的场景下封装和改造其Deferred,当场景不常用,则需要好好取舍封装的成本和带来的简介的关系了。
Promise是高级接口,事件是低级接口。低级接口可以构造更加复杂的业务场景,高级接口一旦定义不容易变化,但是对于解决复杂问题更加有效。
Promise中多异步协作
介绍Promise提到过,主要是用于解决单个异步操作时的问题,但是当多个异步操作时该怎么处理。
类似EventProxy,这里给出了相对简单的原型实现。
Deferred.prototype.all = function(promises){
var count = promises.length;
var that = this;
var results = [];
promises.forEach(function(promise,i){
promise.then(function(data){
count--;
results[i] = data;
if(count === 0){
that.resole(results);
}
},function(err){
that.reject(err);
});
});
return this.promise;
}
如下,读取多个文件时,all 将多个Promise 重新抽象组合为一个Promise。
var promise1 = readFile("foo.txt","utf-8");
var promise2 = readFile("bar.txt","utf-8");
var deferred = new Deferred();
deferred.all([promise1,promise2]).then(function(results){
// TODO
},function(err){
// TODO
});
这里all()方法抽象为多个异步操作,只有当所有的异步操作成功时,才算成功,如果一个异步操作失败,整个异步操作就算失败。
以上知识点均来自<<深入浅出Node.js>>,更多细节建议阅读书籍:-)