qing

二次元中有一个词叫做立flag,暗示的剧情的发展。比如如果那个角色说了一句“我保证会活着回来”,基本上这个角色就离死不远了。Javascript 最近几年也发展出类似的玩意儿,表示将来的某种状态,叫做 Promises (当然这里没有死这么严重),早期 jQuery 的 Deferred 就是类似于 Promises 的实现(当然jQuery的Deferred不完全符合Promises现在的标准)。

聊聊 Promises 的历史

所以,为什么Promises会出现?Promises最原始的是由 Daniel P. Friedman 和 David Wise 提出的。后来有出现了类似的概念叫做Futures。Futures和Promises的出现是为了解决并行编程中同步的问题。有关他们的介绍可以看这里 Futures and Promises

虽然是搬运 wikipedia 的,还是要说一下一般我们会看到几个词 future, promise, deferred, delay 一般来说这几个词可以等价。但是按照原始的定义,或者更确切的从原始的意义上来解释 Futures 和 Promises 还是有细微的不同的。Futures 指的一个只读的变量的占位符,意思就是说 Futures 作为一个异步操作的符号表示,表示这个地方会有一个异步操作的返回。而 Promises 指一个可以对 Futures 进行设置或者操作的容器。 这在单词的字面的意思也能理解,未来是一种代指之后的某一时刻,而承诺本身就隐性地包含了未来。

异步编程与回调

Futures 和 Promises 的提出就是用来解决异步编程的,所以先来看看javascript是如何解决异步编程问题的。

异步编程是我们执行一个函数的时候,可能本身这个函数的执行是费时间的,可是我们不希望这个操作的执行阻塞了当前的线程,希望这个函数能够立即返回,让这个操作在不影响当前线程的情况下运行,然后在将来的某一时刻,操作完成之后通知当前线程,更新状态。

一般来说,javascript 的异步编程基本上依靠的回调函数,不管是最原始的回调,还是 event-drive的方式,或者基于pub-sub的方式,还是我们现在正在讨论的 Promises,本质上都是依靠了回调函数。只是在方式上简化了原始回调的操作,努力去避免了一些在代码组织上的问题。

原始回调

1
2
3
4
5
6
7
8
9
10
11
12
13
op1(function(){
op2(function(){
op3(function(){
// do something
})
})
})

function op1(callback) {
// some async things
// when finish execute callback();
reuturn; // immediately return
}
1
2
3
4
5
6
7
8
9
10
11
// more simple way

op1(cb1)

function cb1(){
op2(cb2)
}

function cb2(){
op3();
}

这是最原始的方法,把函数当作参数传递给异步操作,等到异步操作完成之后,调用回调函数。典型的例子就是setTimeout。

但是原始的回调函数有一个缺点是,当我们的程序越来越复杂的时候,我们的回调的层数会越来越多,代码的耦合性高,在代码的可维护性上就出现问题。想象一下如果我们有5个ajax请求顺序执行,这样就有五层回调,这样如果当我们突然说不行 我们要去掉中间的两个回调,这样带来的代码量的修改是非常大的。

事件驱动

事件驱动可以很好的解决掉这个原始回调带来的问题,考虑如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function async1(){
async1.trigger('async1-done');
}
function async2(){
async2.trigger('async2-done');
}
function async3(){
async3.trigger('async3-done');
}

async1.on('async1-done', function(){
async2()
})
async2.on('async2-done', function(){
async3()
})
async3.on('async3-done', function(){
// some thing
})

async1();

我们看到这里的代码的耦合程度,从代码的可维护性来说,显然这里的事件驱动会明显好于前一种。

pub-sub(发布订阅)

发布订阅是在事件驱动的基础上,把能触发事件和发布事件统一在一起,便于对事件的管理,这样避免在纯事件驱动中的事件种类不可控性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function async1(){
EventBus.trigger('async1-done');
}
function async2(){
EventBus.trigger('async2-done');
}
function async3(){
EventBus.trigger('async3-done');
}

EventBus.on('async1-done', function(){
async2()
})
EventBus.on('async2-done', function(){
async3()
})
EventBus.on('async3-done', function(){
// some thing
})

async1();

这里我们还能对Eventbus限制,比如只能触发规定事件,或者只能监听规定事件,这样对面对逐渐扩大的项目,不会出现事件混乱的情况出现。

Promises 解决方案

其实不能说Promises更优于上面所说的事件驱动的异步方案或者基于订阅发布的方案。但是Promises带来的更优雅的方式。

Promises 在2007年第一次被Dojo所实现,称为dojo.Deferred。之后CommonJS一直致力于标准化Promises行为,现在最被广泛接受的是Promises/A+。所以这里以Promises/A+为例说明Promises。上面的代码如过用Promises可以表述成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
function async1(resolve, reject) {
if (//sucess condition) {
resolve(//async1result)
} else {
reject(//reason)
}
}

function async2(resolve, reject) {
if (//sucess condition) {
resolve(//async2result)
} else {
reject(//reason)
}
}

function async3(resolve, reject) {
if (//sucess condition) {
resolve(//someresult)
} else {
reject(//reason)
}
}

new Promise(aysnc1)
// basic uses
.then(function(result){
// handler sucess
return callback_result1
}, function(reason){
// error handler
})
// chain on same result
.then(function(callback_result1){
// handler sucess
return callback_result2
}, function(reason){
// error handler
})
// chain on different promise
.then(function(result){
return new Promise(async2)
}, function(reason){
// error handler
})
.then(function(async2_result){
return new Promise(async3)
}, function(reason){
// error handler
})
.then(function(async3_result){
// some thing
}, function(reason){
// error handler
})

这里先说明一下, 在Promise中一共只有三个状态pending, fullfilled, rejected. pending 表示异步操作还在进行,fullfilled表示这个异步操作已经成功,rejected表示这个异步操作失败了。

我们在实现async1时我们传入了两个参数resolve和reject,他们都是函数。resolve表示这个操作成功,他接受一个result作为参数。reject表示这个操作失败,接受一个reason参数。然后在这个Promise的then方法我们会传入两个函数,第一个会在resolve调用的时候触发,表示操作成功之后的回调。第二个会在reject调用的时候触发表示这个操作失败之后处理错误。介绍到这里就算是promise的基本用法了。

但是并不是Promise强大的地方。

同一异步操作的链式调用

promise允许在同一个异步调用上反复的使用then,考虑上面代码第二个then,即使在对于async1这个操作的结果进行链式的调用。第一个then中成功回调接受到async1的返回之后处理,然后再进入到第二个then中的成功回调中。这个第二个回调中的参数就是第一个回调的返回。这样我们可以一步一步的对异步操作的原始结果做链式调用,一步一步的处理数据。大大清晰了整个过程。

1
new Promise(asyncOperation).then(processData1).then(processData2).then(processData3)

注意这里,每次then对象的调用实际上是生成的一个新的Promise对象,并不能将这里的链式调用等同于jQuery中的链式调用,应为jQuery的链式调用每次都返回的是同一个对象。

不同异步操作的链式调用

上面的代码的第三个then显示了对于多个异步流程的处理。现在我们的流程是async1->async2->async3, 在pub-sub那节中已经给出了传统的实现。然后考虑上面Promise的处理。Promise前两个then是对于第一个回调的处理,这个已经提到了。第三个then,我们可以看到这个地方在最后返回了一个async2的Promise对象。然后在第四个then中的success回调中我们就能得到async2的结果,然后再返回async3的Promise对象。这样我们就实现了async1->async2->async3的异步流程的链式操作。如果中间比如async2我们不需要了,则只需要将第三个then删除,其他的地方完全不用修改代码。这样的写法必然要比用pub-sub或者event-drive的方式要简单明了。也一眼能看出数据流的方向。

错误异常处理

你可以直接在异步操作的过程中或者在对结果的处理过程中,抛出任何的错误。Promise都会帮你传递到下一个then的错误处理中。对异常和错误的处理十分的方便

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
new Promise(function(resolve, reject){
throw e
}).then(function(){

}, function(e){

})
new Promise(function(resolve, reject){
resolve(e)
}).then(function(){
throw new Error("This is an error")
}, function(){

}).then(null, function(e){
console.log(e)
})

注意,这里的错误处理不会一直传递下去,只会错误出现的下一个then中的错误的处理中被接收到。再下一个then的那个回调会被触发完全取决于你的返回。

回调如何触发

在异步函数中resolve触发success,reject触发fail。但是如果有多个then,那么then的触发取决于上一个回调的返回。

如果是返回值不是没有then方法的函数或者对象,就会触发success的回调,参数就是这个返回值

如果是throw exception 则会触发fail的回调。

如果返回是个Promise对象则取决于Promise的自身的状态,Promise是fullfill的则触发succes,如果是rejected,则触发fail的回调。然后他们的参数就是这个Promise的value或者reason。

如果返回值是有then方法的函数或者对象,这个就取决于then中的处理。这里then方法有两个参数 resolvePromise和rejectPromise,如果执行resovlePromise(x),则下一个then会触发success,如果是执行rejectPromise(reason)则会触发下一个then的fail回调。

自己实现一个Promise

这里限于篇幅我可能会在之后加入如果根据Promise/A+的规范实现一个Promise,详情可以参考我的这个简单实现future.js