浅谈es6中promise对象的应用场景、局限性以及简单构造

作者 Zhe Wang 日期 2016-11-21
浅谈es6中promise对象的应用场景、局限性以及简单构造

EcmaScript2015推出的promise对象,实际上可以理解为相比jquery的deferred或者其他各种第三方的组件,就Promise/A+规范的一次官方发布,它延续了这些组件的大体思路,但并没有从语言底层机制上对javascript做任何的改动。它依然只是从书写层面,让开发者试图避开所谓的“恶魔金字塔”,从语义层面,让代码显得更有逻辑性。
网上有诸多关于promise的文章,但大多数只是介绍promise的使用方式。这里先简单说下我的理解。
promise对象的构造,大概是这个模样

new Promise(function (resolve, reject) {
setTimeout(resolve, 1000);
}).then(function () {
console.log('print after one second');
});

理解这种写法,首先有必要理解下这种写法的思想。promise认为,我们去做一件事情,无非处于三种状态,正在做(pending),做完并且做成了(fulfilled)以及做完了却没有做成(rejected)。
于是对于promise对象,我们将相关操作放在构造函数中,做成了,我们调用resolve,没有做成,我们调用reject。而resolve/reject的实现,我们在then或者catch中传递进来。
其实几句话,就大概说明了promise的使用。这些网络上很多教程都有介绍,不做过多赘述。

我想说说它的应用场景。
promise当然不可能完全取代jser们“情有独钟”的“恶魔金字塔”,也有适合调用的场景。最适合使用promise的场景,我想就是需要重复异步操作的场景,比如需要接连异步请求多个地址,而且它们之间必须按照一定顺序执行。按照最原始的写法就比如

$.ajax({
url: 'foo',
success: function () {
// to do something
$.ajax({
url: 'bar',
success: function () {
// to do something
$.ajax({
...
})
}
})
}
})

这种写法,至少让不习惯嵌套写法的人看着心累。当然如果完全不考虑可读性,只图方便,也可以

(function (arr) {
if (arr.length) {
let url = arr.shift();
$.ajax({
url: url,
success: arguments.callee.bind(null, arr)
})
}
})(['foo', 'bar', ...])

前提是重复的异步请求的回调不会有额外的操作,使用的场景非常有限。
所以这种场景,最适合的不过用promise。看下promise是怎么写的。

function sendRequest (url) {
return new Promise(function (resolve, reject) {
$.ajax({
url: url,
success: resolve
})
})
}

sendRequest('foo')
.then(function () {
// to do something
return sendRequest('bar');
})
.then(function () {
// to do something
return sendRequest('tux');
})

为了突出重点,这些例子都将reject错误处理给省去了。
从这个例子也说明了promise适用的场景,那就是需要重复地、有序地进行相似的(异步)操作的时候。该场景下适用promise,即可以避免“恶魔金字塔”、避免重复代码,又可以很好的体现逻辑性。
promise的关键点之一在于promise的then方法可以通过回调的返回决定then方法返回的promise对象指向,这也是我们为什么将上述例子中的promise对象生成封装成一个函数的原因。

那promise的局限性在哪里呢,我认为重复地、有序的进行不相似的异步操作,需要在每个then返回初始工作不同的promise对象,这么写显得并不讨巧。与其如此,倒不如老老实实将下一步的操作放置在回调中。

简单的构造
我们也自己尝试去实现一个类promise的promise对象。为了不显得太复杂,也暂且把错误处理给忽略掉,我们也不去深究类似promise的all或者race方法的实现。
我们在边写边思考。首先,我们写出构造函数的模样

function MyPromise (task) {
// 定义对象的resolve队列
this.resolveList = [];


}

MyPromise.prototype.then = function (onFulfilled) {

}

接着,我们考虑promise的特性
首先,promise对象如何在实例化过程中就执行了传入函数task,同时又可以能够知道执行完毕后的回调该干什么(即then的传入参数onFulfilled)。这里我们可以使用到setTimeout(func, 0)来实现。
其次,task作为一个沙盒,我们将onFulfilled传入,怎么知道onFulfilled的返回值。因此,我们不能简单的认为task的第一个参数resolve,就是我们调用then方法时候的定义的onFulfilled。两者之间必然存在一个传递关系。

function MyPromise (task) {
var it;

it = this;

// 定义对象的reolve队列
this.resolveList = [];

// 开辟新的进程异步执行
setTimeout(function () {
// task是一个沙盒,为了拿到onFulfilled的返回,我们相当于做了一次传递
task.call(null, function () {
var result;

if (!it.resolveList.length) {

return;
}

result = it.resolveList.shift().apply(null, arguments);
// 如果返回的不是Mypromise对象,实例化一个基本的Promise对象。
if (!(result instanceof MyPromise)) {

result = new MyPromise(function (resolve) {
resolve();
});
}

result.resolveList = it.resolveList;

});
}, 0);

}

MyPromise.prototype.then = function (onFulfilled) {
this.resolveList.push(onFulfilled);
return this;
}

略显期待的执行了一遍,发现无误。如此,我们实现了一个类promise的最基本的部分。当然,一个完整的promise对象,远比这复杂。
文章的内容不过抛砖引玉,但思考的过程值得记录。