在Node.js中使用promise摆脱回调金字塔

在开始谈论正题之前,我们先来看看下面一段代码:

1
2
3
4
5
6
7
8
9
step1(function (value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
step4(value3, function(value4) {
// Do something with value4
});
});
});
});

是不是感觉很恐怖,随着嵌套的回调函数增加,结尾会有大量的花括号和圆括号 }); 出现。

###Promise### 在javascript中实现异步最简单的方式是Callback。遗憾的是,这种编程方式牺牲了控制流,同时你也不能throw new Error()并在外部捕获异常。 Promise的出现解决了这两个需求,又保持了javascript异步的优势,不同于Fiber这种多线程的实现方式,Promise只是一种编程方式的变化。而无须在底层改变。

CommonJS的规范提到了多种Promise,我们只介绍其中一种的实现q (https://github.com/kriskowal/q)

我们在这里不讲解抽象的Promise规范,这多半是实现者应该关心的,我们直接从示例入手,如果你有兴趣,可以参见Promise/A+

q的核心是一个promise对象的then方法,他接受两个回调方法,一个promise被定义之后有3种状态,pending(过渡状态),fullfilled(完成状态),rejected(错误状态)。一个promise只能是这三种状态种的一种,而无法是他们的混合状态。

  • pending状态可以理解为promise还没有获得确定值,就相当于一个任务还没有完成。
  • fullfilled状态可以理解为完成并返回结果。这时then(onFullfilled, onRejected)的onFullfilled方法会被调用。
  • rejected状态可以理解为错误,并结束。返回错误。这时then(onFullfilled, onRejected)的onRejected方法会被调用。

了解了核心思想后,我们来看一个例子,在这个例子中我们先读取一个json文本文件,然后将其解析成javascript对象,最后这个对象进行修改再保存回去。 按照传统的callback写法,有如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fs.readFile('example.json', function(err, data){
if(err) {
console.log(err):
} else {
try {
var obj = JSON.parse(data);
obj.prop = 'something new';
fs.writeFile('example.json', JSON.stringify(obj), function(error){
if(err) {
console.log(error);
} else {
console.log('success');
}
});
} catch(e) {
console.log(e);
}

}
});

在这个例子中,控制流被切割成多个部分(每次异步都要处理一次错误),并且 JSON.parse 的错误必须在内部捕获,但却不能跑到外部。因为在异步回调中无法抛出异常。 现在当我们使用promise的时候,假设我们有个能够返回一个 promise 对象的 readFilewriteFile 方法。那么上面的代码就可以变成如下形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var promise = readFile();
promise
.then(function(data){
// we don't need to catch error. in other words. we can throw error in this callback.
var obj = JSON.parse(data);
obj.prop = 'something new';
// return a promise. so we can chain the then() method.
return writeFile(JSON.stringify(obj));
})
.then(function(){
console.log('success');
}, function(err){
// all error will fall down here.
console.log(err);
});

上面的例子中,我们首先从 readFile() 方法里获得了一个返回的 promise 对象,然后使用这个对象的 then() 方法。在这里,我们只传入了一个 onFullfilled 回调方法,根据Promise/A+的文档。then() 一定会返回一个 promise 对象,所以我们又连接了一个 then() ,由于这个 then() 是最后一个,所以我们需要在这里提供一个 onRejected() 回调方法来处理所有的错误。在第一个 onFullfilled() 回调方法中,我们返回了一个 promise ,这个 promise 的处理结果将会在下一个 then()onFullfilled() 方法中取得。

这段代码执行的时候,当任意位置抛出异常的时候,最后一个then的 onRejected 回调会被执行。否则一切按照从上至下的顺序执行,整个控制流都十分简洁明了。

因为 then() 方法必须返回一个promise,实际上我们也可以结合同步方法返回一个已经fullfilled的promise 比如下面这个例子

1
2
3
4
5
6
7
8
9
10
11
var promise = readFile();
promise
.then(function(data){
return JSON.parse(data);
})
.then(function(obj){
obj.prop = 'something new';
console.log(obj);
},function(err){
console.log(err);
});

上面例子中第二个then会在第一个then返回之后被执行,因为第一个then返回的时候,由于JSON.parse是同步方法,所以返回了一个值,这个值会被包装成一个fullfilled的promise.

###制作promise的API###

上面例子中我们知道了如何使用promise提供的核心方法 then() 。但是对于平时使用的fs等异步的库我们要怎么才能利用promise呢。 在q的文档中介绍了q-io库,里面将常用的io方法都用promise的模式包装了一遍,在实际使用中,你可以使用那个库的方法。不过我们在这里简单的对fs进行包装,让其支持promise,这样以后遇到任何异步方法,你都可以将其转化。

首先定义改一个readFile方法,返回promise,这里利用了 Qdefer() 方法,创建一个 deferred 对象。这个对象有连个关键的方法 resolvereject() 。当resolve(value)执行之后,promise变成fullfilled状态,fullfilled的值就是value 当 reject(reason) 执行之后,promise变成了rejected状态,reason会被传递到onRejected()方法。

1
2
3
4
5
6
7
8
9
10
11
12
var Q = require('q');
function readFile(callback){
var deferred = Q.defer();
fs.readFile('example.json', function(err, data){
if(err){
deferred.reject(err);
} else {
deferred.resolve(data);
}
});
return deferred.promise.nodeify(callback);
}

注意到这里面我们依然提供了一个callback,用于提供一些需要callback的场合的兼容性,我们利用 promise 对象的nodeify方法来调用这个callback,这个callback可以为undefined。

另外一点需要注意的是,一个promise状态改变之后,不能再次改变,所以,你只能调用一次reject或resolve。

有了这个API,我们便可以像前面例子里那样,使用promise来执行读取文件的操作了。其他异步回调转化成返回promise的异步方法基本上都可以参照这个模式来做。

###一次处理多个promise的###

如果你有几个异步方法,他们都返回promise,并且当这些方法都处理完之后,你才能进行下一步,Q提供了一个all()方法来帮助你消化多个promise。

1
2
3
4
5
6
7
8
9
10
11
Q.all([
readFile('file1.json'),
readFile('file2.json')
])
.then(function(dataArray){
for(var i = 0; i < dataArray.length; i++){
console.log(dataArray[i]);
}
}, function(err){
console.log(err);
});

在这里例子里,我们将一个promise数组传给 all() all返回一个promise,当数组里面的所有promise都为fullfilled状态时,我们的then()方法才会被调用。这时fullfilled值是一个数组,每个元素对应前面promise的fullfilled值。 当任意一个promise变成rejected状态的时候,all的promise会立即reject而不等其他的完成。

###利用promise改写你的项目###

最佳的理解方法便是事件,你可以把一些nodejs的基本异步方法包装成promise,这样你就可以在整个程序的多个地方使用这些方法。并且让你的程序的异步代码看起来更整洁,更容易理解。 阅读 Q的文档 了解更多的API和方法。并在程序中使用这些方法,使你的代码更优美,逻辑更健壮。 阅读 Promise/A+ 。了解promise原理。