JS中Promise/A+规范的实现

1,279 阅读8分钟

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支持链式执行,主要通过以下两个步骤。

  1. 将所有的回调都存到队列中。
  2. 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中的多异步协作问题,分为两部分

  1. 多异步并行流程 Promsie提供了all方法,处理多异步并行流程,只有所有异步操作成功,这个异步操作才算成功,一旦其中一个异步操作失败,整个异步操作就失败。基于promise-base实现了promsie-all。对promise扩展实现了all方法,主要维护一个结果队列,当所有异步流程都成功时,将结果队列返回。

  2. 多异步调用依赖处理
    典型的异步处理场景中,有时会涉及到依赖关系的处理,当前一个的结果是后一个调用的输入时,promise提供了链式调用的方式满足这种需求。

then()方法继续返回Promise对象,以实现链式调用。

  • 在内部维护了一个回调队列,将所有的回调都存到队列中。
  • Promise完成时,逐个执行回调,一旦检测到返回了新的Promise对象,停止执行,然后将当前Deferred对象的promise引用改变为新的Promise对象,并将队列中余下的回调转交给它。

源码地址:Promise,可以对照代码来学习本文内容,有用的话别忘了给个star呀~