javascript是事件驱动的.在实际开发中,浏览器端经常会用到ajax异步回调,或者动画的嵌套执行,服务端更是频繁的使用异步回调,如果每一次的异步回调都写成异步回调嵌套异步回调,就会出现回调黑洞或者回调金字塔,异步代码如下所示:

1
2
3
4
5
6
7
8
doAsync1(function () {
doAsync2(function () {
doAsync3(function () {
doAsync4(function () {
})
})
})
})

其实这样的代码本来是没有问题的,但是如果我们要求你async1 taskasync2 task交换顺序,每次交换逻辑代码会显得很恼人.这样的代码确实也并不好维护.我们可以通过promise来优化异步的任务.

什么是promise

关于什么promise的理解可以参考 http://andyshora.com/promises-angularjs-explained-as-cartoon.html. 正如这篇文章中的父亲和孩子,父亲让孩子去看看天气,这个时候就可以认为父亲给孩子一个promise,这个时候promise处于pending状态.父亲允诺如果天气是晴天,父亲就会带孩子去钓鱼,如果天气是阴雨天,父亲就会和孩子呆在家.这种情况就可以看作promise fulfilled的情况.如果孩子因为下雾等各种原因没有带回天气预报,就可以认为是promise rejected的情况,则呆在家.promise的描述如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
son.getWeater()//promise
.then(
function(d){
//successs
if(d === 'good'){
goFishing();
}else{
stayAtHome();
}
},
function(e){
//fail
stayAtHome();
})

promises表示着一个异步操作.promises交互主要通过它的then方法,可以接受一个参数表示成功回调函数,也可以接受两个参数,第一个表示成功回调函数,第二个表示失败回调函数.值得一提的是,then方法接受的回调函数的返回值则可以是任意的JavaScript对象,包括promises.基于这种机制.promise对象的链式调用就起作用了。

web前端中promise

promise优化多层ajax的执行

比如我们现在有三个ajax任务,为ajax1,ajax2,ajax3.如果安装以前的写法会是这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//ajax1
$.ajax({
url:'targetUrl1',
success:function(d1){
//ajax2
$.ajax({
url:'targetUrl2',
success:function(d2){
//ajax3
$.ajax({
url:'targetUrl3',
success:function(d3){
//终于写完了
}
});
}
});
}
})

这样的异步嵌套代码难读,可维护性差,可能有的人会说我把success的function拿出来写会好点,于是会得到这样的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var cb1 = function(d1){
//ajax1 callback
//make ajax2
$.ajax{
url:'targetUrl2',
success:cb2
}
}
var cb2 = function(d2){
//ajax2 callback
//make ajax3
$.ajax({
url:'targetUrl3',
success:cb3
})
}
var cb3 = function(d3){
//ajax3 callback
}
//ajax1
$.ajax({
url:'targetUrl1',
success:cb1
})

这样的做法隔靴搔痒,代码是好看点了,但是可维护性还不如前面的代码.下面我们看看如何用promise来完成这样的异步嵌套任务(这样我们依赖了jQuery的defer对象,也可以用其他的一些promise库比如q)

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
//ajax1
var asynctask1 = function(){
var dfd = $.Deferred();
$.ajax({
url:'targetUrl1',
success:function(d1){
dfd.resolve();
}
});
return dfd.promise();
}

//ajax2
var asynctask2 = function(){
var dfd = $.Deferred();
$.ajax({
url:'targetUrl2',
success:function(d2){
dfd.resolve();
}
});
return dfd.promise();
}

//ajax3
var asynctask3 = function(){
var dfd = $.Deferred();
$.ajax({
url:'targetUrl3',
success:function(d3){
dfd.resolve();
}
});
return dfd.promise();
}

asynctask1()
.then(function(){
return asynctask2();
})
.then(function(){
return asynctask3();
});

是不是觉得这样的代码变得好很多了,可读性、可维护性都提升了一个档次.代码的逻辑还是asynctask1->asynctask2->asynctask3.这里还是要说明下,promise的方式写这种异步回调最终跟嵌套的写法执行效果是一样的,只是这样的写法更加容易被接受.

promise来做动画

其实动画跟ajax这类都可以理解为一类东西,都是耗时任务,都会有回调函数在里面.假设我们在页面中有一个id为box的div,说我们要实现一个效果,让这个box首先在水平方向上跑到中间,再在垂直方向上跑到中间,最终在页面的中间shake一下,对于这样的过程,和上面的三个asynctask本质是一样的.有了之前的经验,这次我们直接上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
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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
var $box = $("#box");
var shakebox = function(){
var dfd = $.Deferred();
var animationEnd = 'webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend';
$box
.addClass('animated shake')
.one(animationEnd,function(){
$box.remoteClass('animated shake');
dfd.resolve();
})
return dfd.promise();
}
var moveRight = (function(){
var left = 0;
var speed = 120;
var delay = 200;
var halfWindownWidth = $(document).width()/2;
var boxWidth = $box.width();
return function(){
var dfd = $.Deferred();
console.log("move right");
var t = setInterval(function(){
left = left + speed * delay / 1000;
//console.log(left,windownWidth);
if(halfWindownWidth >= left + boxWidth){
$box.css({
left: left
})
}else{
console.log('clear timer');
clearInterval(t);
dfd.resolve();
}

},delay);
return dfd.promise();
}

})();

var moveDown = (function(){
var top = 0;
var speed = 60;
var delay = 200;
var halfWindownHeight = $(document).height()/2;
var boxHeight = $box.height();
return function(){
var dfd = $.Deferred();
console.log("move down");
var t = setInterval(function(){
top = top + speed * delay / 1000;
//console.log(left,windownWidth);
if(halfWindownHeight >= top + boxHeight){
$box.css({
top: top
})
}else{
console.log('clear timer');
clearInterval(t);
dfd.resolve();
}

},delay);
return dfd.promise();
}

})();


moveRight()
.then(function(){
return moveDown();
}).then(function(){
return shakebox();
})

后端(node.js)中promise

这里主要以bulebird为例,展示node开发中解决异步回调问题.比如node读取文件问题.项目目录如下图:
node_promise_dir.png
这里我们需要用npm安装bulebird,在项目目录执行npm install bluebird.
正常的读写文件是这样的:

1
2
3
4
5
6
7
8
var fs = require('fs')

fs.readFile('./testfile1.txt','utf-8',function(err, content1){
console.log('content1',content1);
fs.readFile('./testfile2.txt','utf-8',function(err,content2){
console.log('content2',content2);
})
})

跟前端一样,我们还是要用promise去改进这样的嵌套回调代码,否则一旦嵌套的逻辑的顺序调整一下,则需大幅修改代码.下面我们用类似于前端中jQuery的Deferred对象一样去修改我们readFile函数

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
var fs = require('fs');
var Promise = require('bluebird');

//对fs.readFile函数进行修饰
var readFileAsync = function(path){
return new Promise(function(resolve, reject){
fs.readFile(path,'utf-8',function(err,data){
if(err)
//错误就reject promise
reject(err);
else
//成功就fulfill promise
resolve(data);
})
})
};

readFileAsync('testfile1.txt')//promise
.then(function(data){
//resolve read testfile1.txt
console.log(data);
return readFileAsync('testfile2.txt'); //return promise
})
.then(function(data){
//resolve read testfile2.txt
console.log(data);
return readFileAsync('testfile3.txt'); // return promise
})
.then(function(data){
//resolve read testfile3.txt
console.log(data);
})
.catch(function(e){//catch excpetion
//catch excption
console.log(e);
});

对于bluebird,官方更加推荐使用Promise.promisify方法去promise化回调函数

Promises provide a lot of really cool and powerful guarantees like throw safety which are hard to provide when manually converting APIs to use promises. Thus, whenever it is possible to use the Promise.promisify and Promise.promisifyAll methods - we recommend you use them. Not only are they the safest form of conversion - they also use techniques of dynamic recompilation to introduce very little overhead.


这段来自bluebird官方的说法是bluebird采用了dynamic recompilation.那么我们就按照官方的说法把代码再改改,如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var fs = require('fs');
var Promise = require('bluebird');

var readFileAsync = Promise.promisify(fs.readFile);
readFileAsync('testfile1.txt')
.then(function(data){
console.log(data.toString());
return readFileAsync('testfile2.txt');
})
.then(function(data){
console.log(data.toString())
return readFileAsync('testfile3.txt');
})
.catch(function(e){
console.log(e);
})

补充:这里觉得promisify方法更灵活点,比如对于fs上的读写函数,假定一个特定场景,我们一定需要一个同步的函数,那么这个时候promisify函数返回promise化的函数更灵活点.但是promisifyAll所有的函数感觉相对有点太重了.