值得多聊聊的 Promise 模式,以及它能解决什么问题

Promise 模式是解决 Callback hell 的一个很好的方式, 值得我们再多聊一聊。

什么是 Callback Hell

所谓 Callback Hell,就是我们熟悉的闭包或者说是回调函数,连续多级嵌套,导致代码结构的混乱。 比如这样:

step1(function(value1){
    step2(value1, function(value2){
        step3(value2, function(value3){
            step4(value3, function(value4){
                // Do something with value4
            });
        });
    });
});

这些 callback 的嵌套层级过多的话,会导致代码结构上很容易出错,不易阅读和维护。 甚至有一个网站专门介绍它: http://callbackhell.com/ 感兴趣的同学可以了解一下。

Promise 模式

基于 callback 的这个问题,业界也提出了很多解决方案,其中一个就是 Promise 模式。 关于这个模式,之前的文章中也给大家介绍过。这次更多跟大家要聊的,是一个基于 Promise 模式的实现库 - Q。 它提供了更多完善的接口。

比如:

Q.fcall(function(){
    
	return 2;

}).then(function(val){

	console.log(val);
	//...
	
}).then(function(){
	
	//...

}).done();

Q.fcall 是整个 Promise 链条的入口,是我们调用的第一个函数。 然后一系列的 then 调用,会传入接下来的回调函数。 前一个的返回值可以作为后一个调用的参数。 这样就可以把前面多层嵌套的 callback 结构变成线性的 Promise 结构。

Promise 除了改变调用的语法结构之外,还有什么其他的收益呢。答案是肯定的。 假如我们要写一个单元测试,用于测试数据库访问接口。 我们需要预先插入一系列测试数据, 但数据库 API 的调用需要异步 callback, 如果没有 Promise, 我们可能就会写出这样的代码:

dao.add({"title" : "t1" }, function(success){
	
	dao.add({"title" : "t2" }, function(success){

		dao.add({"title" : "t3" }, function(success){

			done();

		}
	}

});

这还是指插入 3 条测试数据,就已经如此复杂,如果我们要插入几十条数据,这样的语法结构基本无法满足需求。 有人说了,我们可不可以这样并行的写呢:

dao.add({"title" : "t1" }, function(success){

});

dao.add({"title" : "t2" }, function(success){

});

dao.add({"title" : "t3" }, function(success){

});

答案是不行, 因为这个方法是异步执行的, 我们需要等待所有的添加操作都完成,才能进行下一步操作,也就是必须等待 callback 调用,在 callback 中执行下一步操作。 所以这个写法会导致我们程序的逻辑错误。 再来看看使用 Promise 怎么解决这个问题:

var testData = [];

//准备测试数据
for(var i = 0;i < 50;i++) {

    testData.push(function(){
        
        var deferred = Q.defer();

        var info = mockImageInfo();
        dao.add(info, function(){

            deferred.resolve("");

        });

        return deferred.promise;

    });

}

testData.reduce(Q.when, Q("")).then(function(){

	//数据插入完成后,进行操作
}).done();

上面的代码一眼看上去是不是有些不好理解。这里给大家讲解一下, 首先我们开始的 for 循环会创建 50 个函数,每个函数都会返回一个 Promise 实例 - return deferred.promise 。 每个函数做的事情就是调用异步方法添加测试数据。

Q 这个库提供了对异步函数的 Promise 支持, 首先调用 Q.defer() , 然后在异步函数的 callback 方法中调用 deferred.resolve 用于标示这个 Promise 执行成功。 真个函数的逻辑是这样, 我们先调用 dao.add 方法异步添加数据,然后立即调用 return deferred.promise 返回 Promise 实例。 因为这个时候 Promise 具体状态还没有确认,要等到异步方法执行完毕, 调用 deferred.resolve 确认状态后,这时整个 Promise 就算执行完成了。

我们创建好这 50 个函数后, 怎么保证他们顺序执行呢? 接下来调用 testData.reduce 方法, 这个方法是 JS 对 Array 类型提供的内建方法,它的作用就是遍历整个数组, 对每个元素执行一个操作函数, 我们这里是 Q.when, 用于处理数组中所有 Promise 函数的执行。

reduce 方法调用完成后,会保证所有 50 个测试数据都插入到数据库中, 然后调用 then 方法,执行下一步的相关操作。 这时所有 50 个异步调用都确保执行完毕。 想一下如果用我们之前的嵌套是写法插入这 50 个数据,那简直就是噩梦了。 Promise 这种方式我们可以插入任意数量的数据,不会影响代码结构。

Q 的 Github 地址: https://github.com/kriskowal/q

总结

这次文章中和大家重新熟悉了一下 Promise 模式以及它解决的问题, 还给大家介绍了一个实现库 - Q。 另外呢也结合我实际开发过程中给大家列举了一个应用场景。 也是我在给一个工程写单元测试的时候,遇到的一个实际问题。相信通过这个实际的问题,能帮助大家更好的理解 Promise 的应用场景。

如果你觉得这篇文章有帮助,还可以关注微信公众号 swift-cafe,会有更多我的原创内容分享给你~

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章