Promise/Deferred标准
Promise/Deferred模式在2009年时被Kris Zyp抽象为一个提议草案,发布在CommonJS规范中。随着使用Promise/Deferred模式的应用逐渐增多,CommonJS草案目前已经抽象出了Promises/A、Promises/B、Promises/D这样典型的异步Promise/Deferred模型。
优点:在一定程度上缓解回调地狱的问题。
Promises/A
Promise/Deferred模式其实包含两部分,即Promise和Deferred。
Promises/A提议对单个异步操作做出了这样的抽象定义,具体如下所示。
- Promise操作只会处在3种状态的一种:未完成态、完成态和失败态。
- Promise的状态只会出现从未完成态向完成态或失败态转化,不能逆反。完成态和失败态不能互相转化。
- Promise的状态一旦转化,将不能被更改。
Promise/Deferred内部实现
在API的定义上,Promises/A提议是比较简单的。一个Promise对象只要具备then()方法即可。但是对于then()方法,有以下简单的要求。
- 接受完成态、错误态的回调方法。在操作完成或出现错误时,将会调用对应方法。
- 可选地支持progress事件回调作为第三个方法。
- then()方法只接受function对象,其余对象将被忽略。
- then()方法继续返回Promise对象,以实现链式调用
为了演示Promises/A提议,这里我们尝试通过继承Node的events模块来完成一个简单的实现,相关代码如下:
var event = require('events')
var util = require('util')
var EventEmitter = event.EventEmitter
var Promise= function () {
EventEmitter.call(this)
}
util.inherits(Promise, EventEmitter);
// 实现then方法
Promise.prototype.then = function (fulfilledHandler, errorHandler, progressHandler) {
if(typeof fulfilledHandler === 'function') {
this.once('success', fulfilledHandler);
}
if(typeof errorHandler === 'function') {
this.once('error', errorHandler);
}
if(typeof progressHandler === 'function') {
this.once('progress', progressHandler);
}
return this
}
这里看到then()方法所做的事情是将回调函数存放起来。为了完成整个流程,还需要触发执行这些回调函数的地方,实现这些功能的对象通常被称为Deferred,即延迟对象,示例代码如下:
// 定义Deferred构造函数
var Deferred = function () {
this.state = 'unfulfilled';
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);
}
// commonjs标准导出的Promise和Deferred
module.exports = {
Promise,
Deferred
}
这里的状态和方法对应的关系如图:
Promise和Deferred的差别:
- Deferred主要是用于内部,用于维护异步模型的状态;
- Promise则作用于外部,通过then()方法暴露给外部以添加自定义逻辑。
Promise和Deferred的整体关系如图所示。
优点:
与事件发布/订阅模式相比,Promise/Deferred模式的API接口和抽象模型都十分简洁。
它将业务中不可变的部分封装在了Deferred中,将可变的部分交给了Promise。
Promise/A+的promise的基本实现
到这一步,一个基本的符合Promise/A+的promise的基本就实现了
// promise-base.js
var event = require('events')
var util = require('util')
var EventEmitter = event.EventEmitter
// 定义一个Promise构造函数,继承EventEmitter模块
var Promise= function () {
EventEmitter.call(this)
}
util.inherits(Promise, EventEmitter);
// 实现then方法
Promise.prototype.then = function (fulfilledHandler, errorHandler, progressHandler) {
if(typeof fulfilledHandler === 'function') {
this.once('success', fulfilledHandler);
}
if(typeof errorHandler === 'function') {
this.once('error', errorHandler);
}
if(typeof progressHandler === 'function') {
this.once('progress', progressHandler);
}
return this
}
// 定义Deferred构造函数
var Deferred = function () {
this.state = 'unfulfilled';
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);
}
// 生成回调函数
Deferred.prototype.callback = function () {
var that = this
return function (err, file) {
if(err) {
return that.reject(err);
}
that.resolve(file)
}
};
module.exports = {
Promise,
Deferred
}
Promise中的多异步协作
多异步并行流程
对于多次文件的读取场景,all将两个单独的Promise重新抽象组合成一个新的Promise,这里给出了一个简单的原型实现,相关代码如下:
// promise-all.js
var { Promise, Deferred } = require('./promise-base')
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.resolve(results);
}
}, function(err){
that.reject(err);
})
});
return this.promise
}
module.exports = {
Promise,
Deferred
}
这里通过all()方法抽象多个异步操作。只有所有异步操作成功,这个异步操作才算成功,一旦其中一个异步操作失败,整个异步操作就失败。
// demo1.js
var { Deferred } = require('./promise-all')
var fs = require('fs')
var readFile = function(file, encoding) {
var deferred = new Deferred()
fs.readFile(file, encoding, deferred.callback())
return deferred.promise
}
var promise1 = readFile('file1.txt', 'utf-8')
var promise2 = readFile('file2.txt', 'utf-8')
var deferred = new Deferred
deferred.all([promise1, promise2]).then(function(results){
console.log(results)
}, function (err) {
// error
})
运行demo2,得到
异步调用的依赖处理
all()适合无依赖的异步串行执行,但当前一个的结果是后一个调用的输入时,all()方法就无法满足需求了。 这里针对这种场景promise提供了链式调用的写法。链式调用支持promise每一次返回新的promise,响应结果作为回调函数函数传给新的promsie。
要让Promise支持链式执行,主要通过以下两个步骤。
- 将所有的回调都存到队列中。
- Promise完成时,逐个执行回调,一旦检测到返回了新的Promise对象,停止执行,然后将当前Deferred对象的promise引用改变为新的Promise对象,并将队列中余下的回调转交给它。
// promose-chain.js
var Promise = function () {
// 队列用于存储待执行的回调函数
this.queue = [];
this.isPromise = true
}
// then()方法所做的事情是将回调函数存放起来。
Promise.prototype.then = function (fulfilledHandler, errorHandler, progressHandler) {
var handler = {};
if(typeof fulfilledHandler === 'function') {
handler.fulfilled = fulfilledHandler
}
if(typeof errorHandler === 'function') {
handler.error = errorHandler
}
this.queue.push(handler)
return this
}
var Deferred = function () {
this.promise = new Promise();
}
// 完成态
Deferred.prototype.resolve = function (obj) {
var promise = this.promise;
var handler;
while ((handler = promise.queue.shift())) { // 从promsie队列中取出第一个元素,直到取不到为止
if(handler && handler.fulfilled){
var ret = handler.fulfilled(obj) // 触发完成态回调函数
if(ret && ret.isPromise) { // 如果它返回的也是回调函数
ret.queue = promise.queue; // 初始化它的内部队列
this.promise = ret; // 赋值给内部的promise
return;
}
}
}
};
// 失败态
Deferred.prototype.reject = function (err) {
var promise = this.promise;
var handler;
while ((handler = promise.queue.shift())) {
if(handler && handler.error){
var ret = handler.error(err) // 触发失败态回调函数
if(ret && ret.isPromise) { // 如果它返回的也是回调函数
ret.queue = promise.queue; // 初始化它的内部队列
this.promise = ret; // 赋值给内部的promise
return;
}
}
}
};
// 生成回调函数
Deferred.prototype.callback = function () {
var that = this
return function (err, file) {
if(err) {
return that.reject(err);
}
that.resolve(file)
}
};
module.exports = {
Promise,
Deferred
}
// demo2.js
var fs = require('fs');
// var smooth = require('./smooth');
var { Deferred } = require('./promise-chain')
var readFile = function(file, encoding) {
var deferred = new Deferred()
fs.readFile(file, encoding, deferred.callback())
return deferred.promise
}
readFile('file1.txt', 'utf8').then(function(file1) {
return readFile(file1.trim(), 'utf8');
}).then(function(file2) {
console.log(file2)
})
运行demo2得到
运行结果如预期!
将API Promise化
这里仍然会发现,为了体验更好的API,需要做较多的准备工作。这里提供了一个方法可以批量将方法Promise化,相关代码如下:
// smooth.js
const { Deferred } = require("./promise-chain")
var smooth = function (method) {
return function () {
var deferred = new Deferred()
var args = Array.prototype.slice.call(arguments, 0) // 获取参数列表:数组的深拷贝(从0开始截取到数组结束的所有元素)
args.push(deferred.callback()); // 构造参数,将deferred.callback作为回调函数(一般作为最后一个参数)
method.apply(null, args) // 绑定到method
return deferred.promise
}
}
module.exports = smooth
如此我们便得到了一个简单的方法将以回调函数运行的异步方法promsie化
改写demo2
// import { Deferred } from 'promise-chain'
var fs = require('fs');
var smooth = require('./smooth');
var { Deferred } = require('./promise-chain')
var readFile = smooth(fs.readFile)
readFile('file1.txt', 'utf8').then(function(file1) {
return readFile(file1.trim(), 'utf8');
}).then(function(file2) {
console.log(file2)
})
运行demo2得到
运行仍然如预期!
写在最后
本来先从Promise/A+的标准的层面表述了一个Promise的抽象定义,即:
- Promise操作只会处在3种状态的一种:未完成态、完成态和失败态
- Promise的状态一旦转化,将不能被更改。。
- Promise的状态只会出现从未完成态向完成态或失败态转化,不能逆反。完成态和失败态不能互相转化。
接着从Promise和Deffered两部分分别实现了对应部分的的内部原理,手写实现了一个基本的promise。主要表述了 Promise/Deferred模式其实包含两部分,即Promise和Deferred。
- Deferred主要是用于内部,主要是触发执行这些回调函数的地方,用于维护异步模型的状态;
- Promise则作用于外部,then()方法所做的事情是将回调函数存放起来,通过then()方法暴露给外部以添加自定义逻辑。
接着讨论了Promise中的多异步协作问题,分为两部分
-
多异步并行流程 Promsie提供了all方法,处理多异步并行流程,只有所有异步操作成功,这个异步操作才算成功,一旦其中一个异步操作失败,整个异步操作就失败。基于promise-base实现了promsie-all。对promise扩展实现了all方法,主要维护一个结果队列,当所有异步流程都成功时,将结果队列返回。
-
多异步调用依赖处理
典型的异步处理场景中,有时会涉及到依赖关系的处理,当前一个的结果是后一个调用的输入时,promise提供了链式调用的方式满足这种需求。
then()方法继续返回Promise对象,以实现链式调用。
- 在内部维护了一个回调队列,将所有的回调都存到队列中。
- Promise完成时,逐个执行回调,一旦检测到返回了新的Promise对象,停止执行,然后将当前Deferred对象的promise引用改变为新的Promise对象,并将队列中余下的回调转交给它。
源码地址:Promise,可以对照代码来学习本文内容,有用的话别忘了给个star呀~