遇见Promise

相信大家对异步都不陌生,在Javascript中处理异步的一种常见的办法就是callback(回调),顾名思义,就是完成之后回来调用的函数。看一个例子

1
2
3
4
5
6
getAsync("fileA.txt", function(error, result){
if(error){// 取得失败时的处理
throw error;
}
// 取得成功时的处理
});

这里的getAsync就是一个异步调用,第二个参数function就是一个回调函数。回调函数的定义在异步函数体的外边。在异步函数体里面调用,这个过程可以和DOM中的事件相类比,getAsync类似于一个DOM事件,等到这个事件触发的时候,会调用callback函数。我们并不知道DOM事件什么时候触发,这里我们也不知道读取文件什么时候结束。但是结束后的行为,比方说将DOM添加selected类,讲文件内容打印等等行为我们要事先定义,让系统在事件触发或者读取文件完毕时调用。

如果回调函数里面又有异步操作,WTF!针对异步调用的成功或者失败有不同的处理,这个时候我们希望代码要好看,理解起来要轻松。

张新旭在ES6 JavaScript Promise的感性认知举了一个例子

“小妞妞,嫁给我吧!我发誓,我会对你一辈子好的!”
“这个嘛,你先去问问我爸爸,我大伯以及我大姑的意思,他们全部都认可你,我再考虑考虑!对了,如果我爸没有答复,大 伯他们肯定是不会同意的;如果大伯没有答复,大姑也是不会同意的。”

  1. 买些好烟好酒登门拜访岳父大人,恳请岳父大人把女儿许配给自己;1日后,得到岳父答复,如果同意,攻略大伯;如果不 同 意,继续攻略1次,要是还不行,失败告终;

  2. 同上攻略大伯;

  3. 买上等化妆品,然后同上攻略大姑;

  4. 拿着满满的offer拿下女神。

这个异步例子告诉我们,清晰的思路是拿下女神的必要条件。那我们赶快学习Promise吧!

Promise的原理

我们的目的是要解决层层嵌套的问题,要让代码看起来是这个样子的:
doSomething()
.then(doAnotherThing)
.then(doThirdThing);

从上面的这个表达我们可以看到,doSomething()返回的应该是一个对象,然后这个对象应该有一个then方法。

设计promise对象

为这个对象设计状态,有两种状态一种是正在运行,还没有结果,一种是运行结束有结果了,运行结束又会有两种状态,成功,或者发生错误。所以设计三种状态(ES6中的Promise可能并不是这样,这里是参考的其他版本的实现)

  • pending: 还没有得到肯定或者失败结果,进行中
  • fulfilled: 成功的操作
  • rejected: 失败的操作

promise对象有一个then方法,表示异步调用结束后做什么。异步调用会有两种结果,成功或者错误,那么then方法也设计成接收两个参数,一个是成功做什么(做什么就是一个函数fullfillFunc),一个是失败做什么(rejectedFunc)。接下来怎么办呢?

doSomething是一个异步函数,那么他会有一个异步调用的地方,然而我们需要异步调用的函数不是放在doSomething里面,而是放在then里面。我们希望promise能够在异步调用结束的时候,能够做一个状态的标记,也就是讲pending状态改为fulfilled(rejected也是一样的,不赘述)。当状态改变的时候,调用指定的回调函数。

所以我们需要有一个地方存储回调函数,设计两个数组一个success,一个fail,当状态改变的时候调用这两个数组中某一个的所有函数(回调函数)。那么这个then做什么呢,嘿嘿,它就是将回调函数push到这两个数组当中。注意这里是异步调用,所以doSomething的完成一定是在then调用之后。

状态改变这样的行为需要一个函数来操作,pending到fulfilled用resolve()表示,pending到rejected用reject()表示。

分析例子

angular中的$q

angular中的$q就是一个Promise,并且是Promise较为经典的实现。我们看一下它的例子。

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
// for the purpose of this example let's assume that
// variables `$q` and `okToGreet`
// are available in the current lexical scope
// (they could have been injected or passed in).

function asyncGreet(name) {
// perform some asynchronous operation, resolve or
// reject the promise when appropriate.
return $q(function(resolve, reject) {
setTimeout(function() {
if (okToGreet(name)) {
resolve('Hello, ' + name + '!');
} else {
reject('Greeting ' + name + ' is not allowed.');
}
}, 1000);
});
}

var promise = asyncGreet('Robin Hood');
promise.then(function(greeting) {
alert('Success: ' + greeting);
}, function(reason) {
alert('Failed: ' + reason);
});

看这个例子,好像和我们刚刚设计的对象有点出入啊。我们仔细分析一下。

首先注意到var promise = asyncGreet(‘Robin Hood’); asyncGreet返回一个Promise对象,这个Promise对象,用then函数push了两个函数到$q的成功函数回调数组和错误函数回调数组当中。嗯,一切看起来都不错。

这个asyncGreet怎么这么复杂! 调用这个函数实际上是调用$q()这个函数,这个函数有一个参数也是一个函数,然后返回的是Promise对象。像不像工厂模式?

这个构造函数接收一个参数是怎么回事。这个参数就是我们的dosomething()吗?看起来不是哟,dosomething可不会有resolve和reject参数。里面的setTimeout才是都something。这里的这个匿名函数参数是做了一个包装。总不能你传什么参数进去,$q都给你构造一个Promise呀。应为我们要在函数里面改变Promise的状态,所以我们要往dosomething里面掺一点沙子进去。包装一下,这个匿名函数会在$q里面调用,然后传入两个回调函数,这两个回调函数用来改变状态。这两个函数的调用就和我们之前写回调一样,成功的时候回调,错误的时候回调另外一个。 好吧搞半天我们就是把then里面的两个匿名函数换成reslove和reject嘛。

是的,事实就是这样。我们做了一次封装就是为了将then里面的函数和上面的setTimeout解耦,他们是有关系,解耦了还是需要要有黏合剂的,就是resolve和reject,以及这个Promise对象。

再看另外一种写法

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
// for the purpose of this example let's assume
// that variables `$q` and `okToGreet`
// are available in the current lexical scope
// (they could have been injected or passed in).

function asyncGreet(name) {
var deferred = $q.defer();

setTimeout(function() {
deferred.notify('About to greet ' + name + '.');

if (okToGreet(name)) {
deferred.resolve('Hello, ' + name + '!');
} else {
deferred.reject('Greeting ' + name + ' is not allowed.');
}
}, 1000);

return deferred.promise;
}

var promise = asyncGreet('Robin Hood');
promise.then(function(greeting) {
alert('Success: ' + greeting);
}, function(reason) {
alert('Failed: ' + reason);
}, function(update) {
alert('Got notification: ' + update);
});

这里首先通过defer()生成一个Promise对象。然后在异步函数中该表Promise的状态,进而调用then里面push到success和error数组里面的函数。当然你可以不需要这里的peomise,将then的两个参数放在setTimeout的对应位置。

推荐阅读 ES6中Promise的详解