这篇文章,咱们一块儿探索一下 JavaScript 中的 Deferred 和 Promise 的概念,它们是 JavaScript 工具包(如Dojo和MochiKit)中很是重要的一个功能,最近也首次亮相于 流行的 JavaScript 库 jQuery(已是1.5版本的事情了)。 Deferred 提供了一个抽象的非阻塞的解决方案(如 Ajax 请求的响应),它建立一个 “promise” 对象,其目的是在将来某个时间点返回一个响应。若是您以前没有接触过 “promise”,咱们将会在下面作详细介绍。javascript
抽象来讲,deferreds 能够理解为表示须要长时间才能完成的耗时操做的一种方式,相比于阻塞式函数它们是异步的,而不是阻塞应用程序等待其完成而后返回结果。deferred对 象会当即返回,而后你能够把回调函数绑定到deferred对象上,它们会在异步处理完成后被调用。php
Promise
你可能已经阅读过一些关于promise和deferreds实现细节的资料。在本章节中,咱们大体介绍下promise如何工做,这些在几乎全部的支持deferreds的javascript框架中都是适用的。html
通常状况下,promise做为一个模型,提供了一个在软件工程中描述延时(或未来)概念的解决方案。它背后的思想咱们已经介绍过:不是执行一个方法而后阻塞应用程序等待结果返回,而是返回一个promise对象来知足将来值。前端
举一个例子会有助于理解,假设你正在建设一个web应用程序, 它很大程度上依赖第三方api的数据。那么就会面临一个共同的问题:咱们没法获悉一个API响应的延迟时间,应用程序的其余部分可能会被阻塞,直到它返回 结果。Deferreds 对这个问题提供了一个更好的解决方案,它是非阻塞的,而且与代码彻底解耦 。java
Promise/A提议’定义了一个’then‘方法来注册回调,当处理函数返回结果时回调会执行。它返回一个promise的伪代码看起来是这样的:react
- promise = callToAPI( arg1, arg2, ...);
- promise.then(function( futureValue ) {
- });
- promise.then(function( futureValue ) {
-
- });
此外,promise回调会在处于如下两种不一样的状态下执行:jquery
- resolved:在这种状况下,数据是可用
- rejected:在这种状况下,出现了错误,没有可用的值
幸运的是,'then'方法接受两个参数:一个用于promise获得了解决(resolved),另外一个用于promise拒绝(rejected)。让咱们回到伪代码:web
- promise.then( function( futureValue ) {
- } , function() {
- } );
在某些状况下,咱们须要得到多个返回结果后,再继续执行应用程序(例如,在用户能够选择他们感兴趣的选项前,显示一组动态的选项)。这种状况下,'when'方法能够用来解决全部的promise都知足后才能继续执行的场景。ajax
- when(
- promise1,
- promise2,
- ...
- ).then(function( futureValue1, futureValue2, ... ) {
-
- });
一个很好的例子是这样一个场景,你可能同时有多个正在运行的动画。 若是不跟踪每一个动画执行完成后的回调,很难作到在动画完成后执行下一步任务。然而使用promise和‘when’方式却能够很直截了当的表示: 一旦动画执行完成,就能够执行下一步任务。最终的结果是咱们能够能够简单的用一个回调来解决多个动画执行结果的等待问题。 例如:json
- when( function(){
- }, function(){
-
- } ).then(function(){
-
- });
这意味着,基本上能够用非阻塞的逻辑方式编写代码并异步执行。 而不是直接将回调传递给函数,这可能会致使紧耦合的接口,经过promise模式能够很容易区分同步和异步的概念。
在下一节中,咱们将着眼于jQuery实现的deferreds,你可能会发现它明显比如今所看到的promise模式要简单。
jQuery的Deferreds
jQuery在1.5版本中首次引入了deferreds。它 所实现的方法与咱们以前描述的抽象的概念没有大的差异。原则上,你得到了在将来某个时候获得‘延时’返回值的能力。在此以前是没法单独使用的。 Deferreds 做为对ajax模块较大重写的一部分添加进来,它遵循了CommonJS的promise/ A设计。1.5和先前的版本包含deferred功能,可使$.ajax() 接收调用完成及请求出错的回调,但却存在严重的耦合。开发人员一般会使用其余库或工具包来处理延迟任务。新版本的jQuery提供了一些加强的方式来管理 回调,提供更加灵活的方式创建回调,而不用关心原始的回调是否已经触发。 同时值得注意的是,jQuery的递延对象支持多个回调绑定多个任务,任务自己能够既能够是同步也能够是异步的。
您能够浏览下表中的递延功能,有助于了解哪些功能是你须要的:
jQuery.Deferred() |
建立一个新的Deferred对象的构造函数,能够带一个可选的函数参数,它会在构造完成后被调用。 |
jQuery.when() |
经过该方式来执行基于一个或多个表示异步任务的对象上的回调函数 |
jQuery.ajax() |
执行异步Ajax请求,返回实现了promise接口的jqXHR对象 |
deferred.then(resolveCallback,rejectCallback) |
添加处理程序被调用时,递延对象获得解决或者拒绝的回调。 |
deferred.done() |
当延迟成功时调用一个函数或者数组函数. |
deferred.fail() |
当延迟失败时调用一个函数或者数组函数.。 |
deferred.resolve(ARG1,ARG2,...) |
调用Deferred对象注册的‘done’回调函数并传递参数 |
deferred.resolveWith(context,args) |
调用Deferred对象注册的‘done’回调函数并传递参数和设置回调上下文 |
deferred.isResolved |
肯定一个Deferred对象是否已经解决。 |
deferred.reject(arg1,arg2,...) |
调用Deferred对象注册的‘fail’回调函数并传递参数 |
deferred.rejectWith(context,args) |
调用Deferred对象注册的‘fail’回调函数并传递参数和设置回调上下文 |
deferred.promise() |
返回promise对象,这是一个伪造的deferred对象:它基于deferred而且不能改变状态因此能够被安全的传递 |
jQuery延时实现的核心是jQuery.Deferred:一个能够链式调用的构造函数。...... 须要注意的是任何deferred对象的默认状态是unresolved, 回调会经过 .then() 或 .fail()方法添加到队列,并在稍后的过程当中被执行。
下面这个$.when() 接受多个参数的例子
- function successFunc(){ console.log( “success!” ); }
- function failureFunc(){ console.log( “failure!” ); }
-
- $.when(
- $.ajax( "/main.php" ),
- $.ajax( "/modules.php" ),
- $.ajax( “/lists.php” )
- ).then( successFunc, failureFunc );
在$.when() 的实现中有趣的是,它并不是仅能解析deferred对象,还能够传递不是deferred对象的参数,在处理的时候会把它们当作deferred对象并立 即执行回调(doneCallbacks)。 这也是jQuery的Deferred实现中值得一提的地方,此外,deferred.then()还为deferred.done和 deferred.fail()方法在deferred的队列中增长回调提供支持。
利用前面介绍的表中提到的deferred功能,咱们来看一个代码示例。 在这里,咱们建立一个很是基本的应用程序:经过$.get方法(返回一个promise)获取一条外部新闻源(1)而且(2)获取最新的一条回复。 同时程序还经过函数(prepareInterface())实现新闻和回复内容显示容器的动画。
为了确保在执行其余相关行为前,上面的这三个步骤确保完成,咱们使用$.when()。根据您的须要 .then()和.fail() 处理函数能够被用来执行其余程序逻辑。
- function getLatestNews() {
- return $.get( “latestNews.php”, function(data){
- console.log( “news data received” );
- $( “.news” ).html(data);
- } );
- }
- function getLatestReactions() {
- return $.get( “latestReactions.php”, function(data){
- console.log( “reactions data received” );
- $( “.reactions” ).html(data);
- } );
- }
-
- function prepareInterface() {
- return $.Deferred(function( dfd ) {
- var latest = $( “.news, .reactions” );
- latest.slideDown( 500, dfd.resolve );
- latest.addClass( “active” );
- }).promise();
- }
-
- $.when(
- getLatestNews(), getLatestReactions(), prepareInterface()
- ).then(function(){
- console.log( “fire after requests succeed” );
- }).fail(function(){
- console.log( “something went wrong!” );
- });
deferreds在ajax的幕后操做中使用并不意味着它们没法在别处使用。 在本节中,咱们将看到在一些解决方案中,使用deferreds将有助于抽象掉异步的行为,并解耦咱们的代码。
异步缓存
当涉及到异步任务,缓存能够是一个有点苛刻的,由于你必须确保对于同一个key任务仅执行一次。所以,代码须要以某种方式跟踪入站任务。 例以下面的代码片断:
- $.cachedGetScript( url, callback1 );
- $.cachedGetScript( url, callback2 );
缓存机制须要确保 脚本无论是否已经存在于缓存,只能被请求一次。 所以,为了缓存系统能够正确地处理请求,咱们最终须要写出一些逻辑来跟踪绑定到给定url上的回调。
值得庆幸的是,这刚好是deferred所实现的那种逻辑,所以咱们能够这样来作:
- var cachedScriptPromises = {};
- $.cachedGetScript = function( url, callback ) {
- if ( !cachedScriptPromises[ url ] ) {
- cachedScriptPromises[ url ] = $.Deferred(function( defer ) {
- $.getScript( url ).then( defer.resolve, defer.reject );
- }).promise();
- }
- return cachedScriptPromises[ url ].done( callback );
- };
代码至关简单:咱们为每个url缓存一个promise对象。 若是给定的url没有promise,咱们建立一个deferred,并发出请求。 若是它已经存在咱们只须要为它绑定回调。 该解决方案的一大优点是,它会透明地处理新的和缓存过的请求。 另外一个优势是一个基于deferred的缓存 会优雅地处理失败状况。 当promise以‘rejected’状态结束的话,咱们能够提供一个错误回调来测试:
$.cachedGetScript( url ).then( successCallback, errorCallback );
请记住:不管请求是否缓存过,上面的代码段都会正常运做!
通用异步缓存
为了使代码尽量的通用,咱们创建一个缓存工厂并抽象出实际须要执行的任务:
- $.createCache = function( requestFunction ) {
- var cache = {};
- return function( key, callback ) {
- if ( !cache[ key ] ) {
- cache[ key ] = $.Deferred(function( defer ) {
- requestFunction( defer, key );
- }).promise();
- }
- return cache[ key ].done( callback );
- };
- }
如今具体的请求逻辑已经抽象出来,咱们能够从新写cachedGetScript:
- $.cachedGetScript = $.createCache(function( defer, url ) {
- $.getScript( url ).then( defer.resolve, defer.reject );
- });
每次调用createCache将建立一个新的缓存库,并返回一个新的高速缓存检索函数。如今,咱们拥有了一个通用的缓存工厂,它很容易实现涉及从缓存中取值的逻辑场景。
图片加载
另外一个候选场景是图像加载:确保咱们不加载同一个图像两次,咱们可能须要加载图像。 使用createCache很容易实现:
- $.loadImage = $.createCache(function( defer, url ) {
- var image = new Image();
- function cleanUp() {
- image.onload = image.onerror = null;
- }
- defer.then( cleanUp, cleanUp );
- image.onload = function() {
- defer.resolve( url );
- };
- image.onerror = defer.reject;
- image.src = url;
- });
接下来的代码片断以下:
- $.loadImage( "my-image.png" ).done( callback1 );
- $.loadImage( "my-image.png" ).done( callback2 );
不管image.png是否已经被加载,或者正在加载过程当中,缓存都会正常工做。
缓存数据的API响应
哪些你的页面的生命周期过程当中被认为是不可变的API请求,也是缓存完美的候选场景。 好比,执行如下操做:
- $.searchTwitter = $.createCache(function( defer, query ) {
- $.ajax({
- url: "http://search.twitter.com/search.json",
- data: { q: query },
- dataType: "jsonp",
- success: defer.resolve,
- error: defer.reject
- });
- });
程序容许你在Twitter上进行搜索,同时缓存它们:
- $.searchTwitter( "jQuery Deferred", callback1 );
- $.searchTwitter( "jQuery Deferred", callback2 );
定时
基于deferred的缓存并不限定于网络请求;它也能够被用于定时目的。
例如,您可能须要在网页上给定一段时间后执行一个动做,来吸引用户对某个不容易引发注意的特定功能的关注或处理一个延时问题。 虽然setTimeout适合大多数用例,但在计时器出发后甚至理论上过时后就没法提供解决办法。 咱们可使用如下的缓存系统来处理:
- var readyTime;
- $(function() { readyTime = jQuery.now(); });
- $.afterDOMReady = $.createCache(function( defer, delay ) {
- delay = delay || 0;
- $(function() {
- var delta = $.now() - readyTime;
- if ( delta >= delay ) { defer.resolve(); }
- else {
- setTimeout( defer.resolve, delay - delta );
- }
- });
- });
新的afterDOMReady辅助方法用最少的计数器提供了domReady后的适当时机。 若是延迟已通过期,回调会被立刻执行。
同步多个动画
动画是另外一个常见的异步任务范例。 然而在几个不相关的动画完成后执行代码仍然有点挑战性。尽管在jQuery1.6中才提供了在动画元素上取得promise对象的功能,但它是很容易的手动实现:
- $.fn.animatePromise = function( prop, speed, easing, callback ) {
- var elements = this;
- return $.Deferred(function( defer ) {
- elements.animate( prop, speed, easing, function() {
- defer.resolve();
- if ( callback ) {
- callback.apply( this, arguments );
- }
- });
- }).promise();
- };
而后,咱们可使用$.when()同步化不一样的动画:
- var fadeDiv1Out = $( "#div1" ).animatePromise({ opacity: 0 }),
- fadeDiv2In = $( "#div1" ).animatePromise({ opacity: 1 }, "fast" );
-
- $.when( fadeDiv1Out, fadeDiv2In ).done(function() {
-
- });
咱们也可使用一样的技巧,创建了一些辅助方法:
- $.each([ "slideDown", "slideUp", "slideToggle", "fadeIn", "fadeOut", "fadeToggle" ],
- function( _, name ) {
- $.fn[ name + "Promise" ] = function( speed, easing, callback ) {
- var elements = this;
- return $.Deferred(function( defer ) {
- elements[ name ]( speed, easing, function() {
- defer.resolve();
- if ( callback ) {
- callback.apply( this, arguments );
- }
- });
- }).promise();
- };
- });
而后想下面这样使用新的助手代码来同步动画:
- $.when(
- $( "#div1" ).fadeOutPromise(),
- $( "#div2" ).fadeInPromise( "fast" )
- ).done(function() {
-
- });
-
一次性事件
虽然jQuery提供你可能须要的全部的时间绑定方法,但当事件仅须要处理一次时,状况可能会变得有点棘手。( 与$.one() 不一样 )
例如,您可能但愿有一个按钮,当它第一次被点击时打开一个面板,面板打开以后,执行特定的初始化逻辑。 在处理这种状况时,人们一般会这样写代码:
- var buttonClicked = false;
- $( "#myButton" ).click(function() {
- if ( !buttonClicked ) {
- buttonClicked = true;
- initializeData();
- showPanel();
- }
- });
不久后,你可能会在面板打开以后点击按钮时添加一些操做,以下:
这是一个很是耦合的解决办法。 若是你想添加一些其余的操做,你必须编辑绑定代码或拷贝一份。 若是你不这样作,你惟一的选择是测试buttonClicked。因为buttonClicked多是false,新的代码可能永远不会被执行,所以你 可能会失去这个新的动做。
使用deferreds咱们能够作的更好 (为简化起见,下面的代码将只适用于一个单一的元素和一个单一的事件类型,但它能够很容易地扩展为多个事件类型的集合):
- $.fn.bindOnce = function( event, callback ) {
- var element = $( this[ 0 ] ),
- defer = element.data( "bind_once_defer_" + event );
- if ( !defer ) {
- defer = $.Deferred();
- function deferCallback() {
- element.unbind( event, deferCallback );
- defer.resolveWith( this, arguments );
- }
- element.bind( event, deferCallback )
- element.data( "bind_once_defer_" + event , defer );
- }
- return defer.done( callback ).promise();
- };
该代码的工做原理以下:
- 检查该元素是否已经绑定了一个给定事件的deferred对象
- 若是没有,建立它,使它在触发该事件的第一时间解决
- 而后在deferred上绑定给定的回调并返回promise
代码虽然很冗长,但它会简化相关问题的处理。 让咱们先定义一个辅助方法:
- $.fn.firstClick = function( callback ) {
- return this.bindOnce( "click", callback );
- };
而后,以前的逻辑能够重构以下:
- var openPanel = $( "#myButton" ).firstClick();
- openPanel.done( initializeData );
- openPanel.done( showPanel );
若是咱们须要执行一些动做,只有当面板打开之后,全部咱们须要的是这样的:
- openPanel.done(function() {
若是面板没有打开,行动将获得延迟到单击该按钮时。
组合助手
单独看以上每一个例子,promise的做用是有限的 。 然而,promise真正的力量是把它们混合在一块儿。
在第一次点击时加载面板内容并打开面板
假如,咱们有一个按钮,能够打开一个面板,请求其内容而后淡入内容。使用咱们前面定义的助手方法,咱们能够这样作:
- var panel = $( "#myPanel" );
- panel.firstClick(function() {
- $.when(
- $.get( "panel.html" ),
- panel.slideDownPromise()
- ).done(function( ajaxResponse ) {
- panel.html( ajaxResponse[ 0 ] ).fadeIn();
- });
- });
在第一次点击时载入图像并打开面板
假如,咱们已经的面板有内容,但咱们只但愿当第一次单击按钮时加载图像而且当全部图像加载成功后淡入图像。HTML代码以下:
- <div id="myPanel">
- <img data-src="image1.png" />
- <img data-src="image2.png" />
- <img data-src="image3.png" />
- <img data-src="image4.png" />
- </div>
咱们使用data-src属性描述图片的真实路径。 那么使用promise助手来解决该用例的代码以下:
- $( "#myButton" ).firstClick(function() {
- var panel = $( "#myPanel" ),
- promises = [];
- $( "img", panel ).each(function() {
- var image = $( this ), src = element.attr( "data-src" );
- if ( src ) {
- promises.push(
- $.loadImage( src ).then( function() {
- image.attr( "src", src );
- }, function() {
- image.attr( "src", "error.png" );
- } )
- );
- }
- });
-
- promises.push( panel.slideDownPromise() );
-
- $.when.apply( null, promises ).done(function() { panel.fadeIn(); });
- });
这里的窍门是跟踪全部的LoadImage 的promise,接下来加入面板slideDown动画。 所以首次点击按钮时,面板将slideDown而且图像将开始加载。 一旦完成向下滑动面板和已加载的全部图像,面板才会淡入。
在特定延时后加载页面上的图像
假如,咱们要在整个页面实现递延图像显示。 要作到这一点,咱们须要的HTML的格式以下:
- <img data-src="image1.png" data-after="1000" src="placeholder.png" />
- <img data-src="image2.png" data-after="1000" src="placeholder.png" />
- <img data-src="image1.png" src="placeholder.png" />
- <img data-src="image2.png" data-after="2000" src="placeholder.png" />
意思很是简单:
- image1.png,第三个图像当即显示,一秒后第一个图像显示
- image2.png 一秒钟后显示第二个图像,两秒钟后显示第四个图像
咱们将如何实现呢?
- $( "img" ).each(function() {
- var element = $( this ),
- src = element.attr( "data-src" ),
- after = element.attr( "data-after" );
- if ( src ) {
- $.when(
- $.loadImage( src ),
- $.afterDOMReady( after )
- ).then(function() {
- element.attr( "src", src );
- }, function() {
- element.attr( "src", "error.png" );
- } ).done(function() {
- element.fadeIn();
- });
- }
- });
若是咱们想延迟加载的图像自己,代码会有所不一样:
- $( "img" ).each(function() {
- var element = $( this ),
- src = element.attr( "data-src" ),
- after = element.attr( "data-after" );
- if ( src ) {
- $.afterDOMReady( after, function() {
- $.loadImage( src ).then(function() {
- element.attr( "src", src );
- }, function() {
- element.attr( "src", "error.png" );
- } ).done(function() {
- element.fadeIn();
- });
- } );
- }
- });
这里,咱们首先在尝试加载图片以前等待延迟条件知足。当你想在页面加载时限制网络请求的数量会很是有意义。
结论
正如你看到的,即便在没有Ajax请求的状况下,promise也很是有用的。经过使用jQuery 1.5中的deferred实现 ,会很是容易的从你的代码中分离出异步任务。 这样的话,你能够很容易的从你的应用程序中分离逻辑。
您可能感兴趣的相关文章
本文连接:使用 jQuery Deferred 和 Promise 建立响应式应用
译者:点点滴滴博客,原文:Creating Responsive Applications Using jQuery Deferred and Promises