没有JavaScript的有关期货/承诺的系列文章将不完整。 在JavaScript中,期货(在JS领域中通常被称为Promise )无处不在,以至于我们几乎不再认识它们。 AJAX,超时和整个Node.JS都是基于异步回调构建的。 嵌套回调(我们将在短短一秒钟内看到)是如此难以遵循和保持,以至于回调地狱术语是被创造出来的。 在这篇文章中,我将解释如何承诺能够提高可读性和模块化代码。
介绍承诺对象
让我们以使用AJAX和$.getJSON()
帮助器方法的第一个最简单的示例为例:
$.getJSON('square/3', function(square) {
console.info(square);
});
square/3
是AJAX资源,产生9
( 3 square )。 我假定您熟悉AJAX,并且了解一旦响应从服务器到达,回调日志记录9
将异步执行。 就这么简单,但是一旦您开始嵌套,链接并希望处理错误,它很快就会变得笨拙:
$.getJSON('square/3', function(threeSquare) {
$.getJSON('square/4', function(fourSquare) {
console.info(threeSquare + fourSquare);
});
});
$.ajax({
dataType: "json",
url: 'square/10',
success: function(square) {
console.info(square);
},
error: function(e) {
console.warn(e);
}
});
突然,业务逻辑被深深地嵌套在嵌套的回调中(事实上,这仍然不错,但是在实践中它会变得更糟)。 回调还有另一个问题–一旦需要回调,几乎不可能编写干净,可重用的组件。 例如,我想用很好的function square(x)
实用程序封装AJAX调用。 但是如何“返回”结果? 通常,开发人员只需要提供回调函数,这绝对是不干净的: function square(x, callbackFun)
。 幸运的是,我们知道了Future / Promise模式,jQuery(从1.5开始,在1.8中有进一步改进)使用CommonJS Promises / A API提议实现了它:
function square(x) {
return $.getJSON('square/' + x);
}
var promise3 = square(3);
//or directly:
var promise3b = $.getJSON('square/3');
square()
或更确切地说$.getJSON()
返回什么? 调用不同步–我们返回一个Promise对象! 我们“保证”结果将在将来的某个时间提供。 我们如何检索该结果? 在Java和Scala中,不鼓励在Future
上进行阻塞。 在jQuery中甚至是不可能的(至少没有API)。 但是我们有一个干净的API用于注册回调:
promise3.done(function(threeSquare) {
console.info(threeSquare);
});
promise3.done(function() {
console.debug("Done");
});
promise3.done(function(threeSquare) {
$('.result').text(threeSquare);
});
那么,有什么区别呢? 首先,我们返回某些内容而不是进行回调–这使代码更易读和悦耳。 其次,我们可以根据需要注册尽可能多的不相关的回调,并且它们都按顺序执行。 最后, promise
对象会记住结果,因此即使我们在 Promise解决(响应到达) 后注册回调,它仍将执行。 但这只是冰山一角。 稍后,我们将看到JavaScript中许诺的各种技术和模式。
结合诺言
首先,您可以轻松地“等待”两个或多个任意承诺:
var promise3 = $.getJSON('square/3');
var promise5 = $.getJSON('square/5');
$.when(promise3, promise5).done(
function(threeSquare, fiveSquare) {
console.info(threeSquare, fiveSquare);
}
);
没有嵌套或状态。 只需获得两个承诺,并在两个结果均可用时让图书馆通知我们。 注意, $.when(promise3, promise5)
返回另一个promise,因此您可以进一步对其进行链式转换。 $.when
一个缺点是它不接受(识别)一系列承诺。 但是JavaScript具有足够的动态性,可以轻松解决它:
var promises = [
$.getJSON('square/2'),
$.getJSON('square/3'),
$.getJSON('square/4'),
$.getJSON('square/5')
];
$.when.apply($, promises).done(function() {
console.info(arguments);
});
如果您发现难以遵循:
- 每个
$.getJSON()
返回一个promise对象,因此promises
是一个promises数组( duh! )。 - 每个已解决的promise作为单独的参数传递,因此我们必须使用
arguments
伪数组来捕获所有参数。 - 当所有的承诺都得到解决(最后的AJAX调用返回)时,将执行
done()
回调,但是承诺可以来自任何来源,而不必来自AJAX请求(请进一步阅读Deferred
对象) -
$.when()
具有完全相同的语义Futures.allAsList()
番石榴和Future.sequence()
在阿卡/斯卡拉 。 - (旁注)同时启动多个AJAX调用不一定是最佳设计,请尝试将它们组合以提高性能和响应能力。
自定义承诺与
我们之前实现了自定义Future
和ListenableFuture
。 许多开发人员感到困惑,promise和$.Deferred
(正是我们需要的时候)之间的区别是什么—实现自定义方法返回诺言,就像$.ajax()
和朋友一样。 除了AJAX之外,众所周知, setTimeout()
和setInterval()
会引入嵌套回调。 我们能兑现承诺吗? 当然!
function timeoutPromise(millis, context) {
var deferred = $.Deferred();
setTimeout(function() {
deferred.resolve(context);
}, millis);
return deferred.promise();
}
var promise = timeoutPromise(1000, 'Boom!');
promise.done(function(s) {
console.info(s);
});
timeoutPromise()
每一行timeoutPromise()
重要,因此请仔细研究。 首先,我们创建$.Deferred()
实例,该实例基本上是尚未解析的值(未来)的容器。 后来我们注册超时时间为AFTER触发器millis
毫秒。 一旦时间过去,我们将解析延迟的对象。 兑现承诺后,将自动调用所有已注册的done
回调。 最后,我们将内部promise
对象返回给客户端。 在下面,您看到了如何使用这种承诺–与AJAX几乎相同。 你能猜出要打印什么吗? 当然,对象表示通过context
中deferred.resolve(context)
调用,那就是'Boom!'
串。
我希望我不必重复自己强调的一点,即我们可以根据需要注册任意数量的回调,如果在promise解决后(超时后)注册回调,它仍将立即执行。
监控进度
承诺很好,但是当我们想使用setInterval()
而不是setTimeout()
时,它们就不合适了。 Future只能解析一次,而setInterval()
可以触发提供的回调多次。 但是jQuery Promise具有我们系列中尚未看到的一项独特功能:进度监视API。 在我们兑现承诺之前,我们可以通知客户其进展情况。 对于长时间运行的多阶段过程而言,这是有意义的。 这是setInterval()
的实用程序:
function intervalPromise(millis, count) {
var deferred = $.Deferred();
if(count <= 0) {
deferred.reject("Negative repeat count " + count);
}
var iteration = 0;
var id = setInterval(function() {
deferred.notify(++iteration, count);
if(iteration >= count) {
clearInterval(id);
deferred.resolve();
}
}, millis);
return deferred.promise();
}
intervalPromise()
重复count
次,每次millis
毫秒。 对deferred.reject()
第一个通知调用将立即使promise失败(请参阅下文)。 其次要注意deferred.notify()
,它在每次迭代时都会调用以通知进度。 这是使用此功能的两种等效方法。 如果Promise被拒绝,将使用fail()
回调:
var notifyingPromise = intervalPromise(500, 4);
notifyingPromise.
progress(function(iteration, total) {
console.debug("Completed ", iteration, "of", total);
}).
done(function() {
console.info("Done");
}).
fail(function(e) {
console.warn(e);
});
要么:
intervalPromise(500, 4).then(
function() {
console.info("Done");
},
function(e) {
console.warn(e);
},
function(iteration, total) {
console.debug("Completed ", iteration, "of", total);
}
);
上面的第二个示例更加紧凑,但可读性稍差。 但是它们都产生完全相同的输出(每500毫秒打印一次进度消息):
Completed 1 of 4
Completed 2 of 4
Completed 3 of 4
Completed 4 of 4
Done
对于多请求AJAX调用,进度通知可能更有意义。 假设您需要执行两个AJAX请求才能完成某些过程。 您想让用户知道整个过程何时完成,还可以让用户知道第一个调用何时完成。 例如,对于构建响应更快的GUI,这可能很有用。 这很容易:
function doubleAjax() {
var deferred = $.Deferred();
$.getJSON('square/3', function(threeSquare) {
deferred.notify(threeSquare)
$.getJSON('square/4', function(fiveSquare) {
deferred.resolve(fiveSquare);
});
});
return deferred.promise();
}
doubleAjax().
progress(function(threeSquare) {
console.info("In the middle", threeSquare);
}).
done(function(fiveSquare) {
console.info("Done", fiveSquare);
});
请注意,一旦第一个请求完成,我们将如何通知promise
并最终解决它。 客户端可以自由地仅处理done()
回调或两者。 使用传统的基于回调的API,我们将获得doubleAjax(doneCallback, progressCallback)
函数,该函数将两个函数作为参数,其中第二个是可选的(?)Progress API在到目前为止我们探索的其他主要语言中不可用,这使jQuery成为可能非常有用和有趣。
链接和转化承诺
我想与您分享的最后一件事是链接和改变承诺。 这个概念对我们来说并不陌生 (在Java和Scala / Akka中都是如此 )。 在JavaScript中看起来如何? 首先定义一些底层方法:
function square(value) {
return $.getJSON('square/' + value);
}
function remoteDouble(value) {
return $.getJSON('double/' + value);
}
function localDouble(x) {
return x * 2;
}
现在,我们可以将它们无缝组合:
square(2).then(localDouble).then(function(doubledSquare) {
console.info(doubledSquare);
});
square(2).then(remoteDouble).then(localDouble).then(function(doubledSquare) {
console.info(doubledSquare);
});
第一个示例在结果到达(2平方)并将其乘以2后应用localDouble()
函数。 因此,最终回调将打印8
。 第二个例子更有趣。 请仔细看。 当square(2)
承诺得到解决时,我们调用remoteDouble(4)
( 4
是异步square/2
AJAX调用的结果)。 但是,此函数再次返回一个承诺。 当第二个promise返回时,最终回调将打印8
( double/4
调用的结果)。 这种结构允许我们通过提供一个调用的结果作为后续调用的参数来链接AJAX调用(和其他承诺)。
AngularJS中的承诺
AngularJS具有一个真正整洁的功能,可以利用动态类型和承诺。 我相信jQuery可以从这个简单的想法中学到很多东西,并且可以在核心库中实现它。 但是回到重点。 这是AngularJS中典型的AJAX交互更新GUI:
angular.module('promise', []);
function Controller($scope, $http) {
$http.get('square/3').success(function(reply) {
$scope.result = {data: reply};
});
}
模板如下:
<body ng-app="promise" ng-controller="Controller">
3 square: {{result.data}}
</body>
如果您不熟悉AngularJS, $scope
分配值会自动更新所有DOM元素,这些元素引用已修改的作用域变量。 因此,运行该应用程序将在响应到达后渲染3 square: 9
。 看起来很干净(注意AngularJS也使用promises!)但是我们可以做得更好! 首先一些代码:
function Controller($scope, $http) {
$scope.result = $http.get('square/3');
}
这段代码比看起来要聪明得多。 请记住, $http.get()
返回一个promise ,而不是一个值。 这意味着我们正在向我们的范围分配承诺(可能尚未收到AJAX响应)。 还是不明白为什么我如此兴奋? 尝试:
`$('.result').text($.getJSON('square/3'))`
在jQuery中。 不会的 。 但是AngularJS足够聪明,可以认识到范围变量实际上是一个承诺。 因此,它没有尝试渲染它(导致[object Object]
),而只是等待它解析。 一旦解决了诺言,它将用其值替换它并更新DOM。 自动地。 无需使用回调,框架将了解我们不希望显示promise,而是一旦解决即显示其价值。 顺便说一下,AngularJS有自己的Deferred
实现,并在$q
service中保证 。
摘要
通过使用promise而不是可怕的回调,我们可以大大简化JavaScript代码。 尽管JS应用程序具有异步特性,但它的外观和感觉更为必要。 而且,正如我们已经看到的那样,期货和承诺的概念存在于许多现代编程语言中,因此,每个程序员都应该熟悉并感到满意。
参考: NoBlogDefFound博客上的JCG合作伙伴 Tomasz Nurkiewicz提供的jQuery和AngularJS中的Promises和Deferred对象 。
翻译自: https://www.javacodegeeks.com/2013/03/promises-and-deferred-objects-in-jquery-and-angularjs.html