jQuery中的Deferred详解和使用
首先,为什么要使用Deferred?
先来看一段AJAX的代码:
var data;$.get('api/data', function(resp) {data = resp.data;});doSomethingFancyWithData(data);
这段代码极容易出问题,请求时间多长或者超时,将会导致我们获取不到data。只有把请求设置为同步我们才能够等待获取到data
,才执行我们的函数。但是这会带来阻塞,导致用户界面一直被冻结,对用户体验有很严重的影响。所以我们需要使用异步编程,
JS的异步编程有两种方式基于事件和基于回调,
传统的异步编程会带来的一些问题,
1.序列化异步操作导致的问题:
1),延续传递风格Continuation Passing Style (CPS)
2),深度嵌套
3),回调地狱
2.并行异步操作的困难
下面是一段序列化异步操作的代码:
// Demonstrates nesting, CPS, 'callback hell'$.get('api1/data', function(resp1) {// Next that depended on the first response.$.get('api2/data', function(resp2) {// Next request that depended on the second response.$.get('api3/data', function(resp3) {// Next request that depended on the third response.$.get(); // ... you get the idea.});});});
当回调越来越多,嵌套越深,代码可读性就会越来越差。如果注册了多个回调,那更是一场噩梦!
再看另一段有关并行化异步操作的代码:
$.get('api1/data', function(resp1) { trackMe(); });
$.get('api2/data', function(resp2) { trackMe(); });
$.get('api3/data', function(resp3) { trackMe(); });
var trackedCount = 0;
function trackMe() {++trackedCount;if (trackedCount === 3) {doSomethingThatNeededAllThree();}
}
上面的代码意思是当三个请求都成功就执行我们的函数(只执行一次),毫无疑问,这段代码有点繁琐,而且如果我们要添加失败回调将会是一件很麻烦的事情。
我们需要一个更好的规范,那就是Promise
规范,这里引用Aaron的一篇文章中的一段,http://www.cnblogs.com/aaronjs/p/3163786.html:
- 在我开始
promise
的“重点”之前,我想我应该给你一点它们如何工作的内貌。一个promise
是一个对象——根据Promise/A
规范——只需要一个方法:then
。then
方法带有三个参数:一个成功回调,一个失败回调,和一个前进回调(规范没有要求包括前进回调的实现,但是很多都实现了)。一个全新的promise
对象从每个then
的调用中返回。 - 一个
promise
可以是三种状态之一:未完成的,完成的,或者失败的。promise
以未完成的状态开始,如果成功它将会是完成态,如果失败将会是失败态。当一个promise
移动到完成态,所有注册到它的成功回调将被调用,而且会将成功的结果值传给它。另外,任何注册到promise
的成功回调,将会在它已经完成以后立即被调用。 - 同样的事情发生在
promise
移动到失败态的时候,除了它调用的是失败回调而不是成功回调。对包含前进特性的实现来说,promise
在它离开未完成状态以前的任何时刻,都可以更新它的progress
。当progress
被更新,所有的前进回调(progress callbacks
)会被传递以progress
的值,并被立即调用。前进回调被以不同于成功和失败回调的方式处理;如果你在一个progress
更新已经发生以后注册了一个前进回调,新的前进回调只会在它被注册以后被已更新的progress
调用。 - 我们不会进一步深入
promise
状态是如何管理的,因为那不在规范之内,而且每个实现都有差别。在后面的例子中,你将会看到它是如何完成的,但目前这就是所有你需要知道的。
现在有不少库已经实现了Deferred
的操作,其中jQuery的Deferred
就非常热门:
先过目一下Deferred
的API:
jQuery的有关Deferred
的API简介:
$.ajax('data/url').done(function(response, statusText, jqXHR){console.log(statusText);}).fail(function(jqXHR, statusText, error){console.log(statusText);}),always(function(){console.log('I will always done.');});
1.done
,fail
,progress
都是给回调列表添加回调,因为jQuery的Deferred
内部使用了其$.Callbacks
对象,并且增加了memory
的标记(详情请查看我的这篇文章jQuery1.9.1源码分析–Callbacks对象),
所以如果我们第一次触发了相应的回调列表的回调即调用了resolve
,resolveWith
,reject
,rejectWith
或者notify
,notifyWith
这些相应的方法,当我们再次给该回调列表添加回调时,就会立刻触发该回调了,即使用了done
,fail
,progress
这些方法,而不需要我们手动触发。jQuery的ajax
会在请求完成后就会触发相应的回调列表。所以我们后面的链式操作的注册回调有可能是已经触发了回调列表才添加的,所以它们就会立刻被执行。
2.always
方法则是不管成功还是失败都会执行该回调。
接下来要介绍重量级的then
方法(也是pipe
方法):
3.then
方法会返回一个新的Deferred
对象
* 如果then
方法的参数是deferred对象,上一链的旧deferred
会调用[ done | fail | progress ]
方法注册回调,该回调内容是:执行then
方法对应的参数回调(fnDone
, fnFail
, fnProgress
)。
*
* 1)如果参数回调执行后返回的结果是一个promise
对象,我们就给该promise
对象相应的回调列表添加回调,该回调是触发then
方法返回的新promise
对象的成功,失败,处理中(done,fail,progress
)的回调列表中的所有回调。
* 当我们再给then
方法进行链式地添加回调操作(done,fail,progress,always,then
)时,就是给新deferred对象注册回调到相应的回调列表。
* 如果我们then
参数fnDoneDefer, fnFailDefer, fnProgressDefer
得到了解决,就会执行后面链式添加回调操作中的参数函数。
*
* 2)如果参数回调执行后返回的结果returned
不是promise
对象,就立刻触发新deferred
对象相应回调列表的所有回调,且回调函数的参数是先前的执行返回结果returned
。
* 当我们再给then
方法进行链式地添加回调操作(done,fail,progress,always,then
)时,就会立刻触发我们添加的相应的回调。
*
* 可以多个then
连续使用,此功能相当于顺序调用异步回调。
$.ajax({url: 't2.html',dataType: 'html',data: {d: 4}}).then(function(){console.log('success');},function(){console.log('failed');}).then(function(){console.log('second');return $.ajax({url: 'jquery-1.9.1.js',dataType: 'script'});}, function(){console.log('second f');return $.ajax({url: 'jquery-1.9.1.js',dataType: 'script'});}).then(function(){console.log('success2');},function(){console.log('failed2');});
上面的代码,如果第一个对t2.html
的请求成功输出success
,就会执行second
的ajax
请求,接着针对该请求是成功还是失败,执行success2
或者failed2
。
如果第一个失败输出failed
,然后执行second
f
的ajax
请求(注意和上面的不一样),接着针对该请求是成功还是失败,执行success2
或者failed2
。
理解这些对失败处理很重要。
将我们上面序列化异步操作的代码使用then方法改造后,代码立马变得扁平化了,可读性也增强了:
var req1 = $.get('api1/data');var req2 = $.get('api2/data');var req3 = $.get('api3/data');req1.then(function(req1Data){return req2.done(otherFunc);}).then(function(req2Data){return req3.done(otherFunc2);}).then(function(req3Data){doneSomethingWithReq3();});
4.接着介绍$.when
的方法使用,主要是对多个deferred
对象进行并行化操作,当所有deferred
对象都得到解决就执行后面添加的相应回调。
$.when($.ajax({url: 't2.html'}),$.ajax({url: 'jquery-1.9.1-study.js'})).then(function(FirstAjaxSuccessCallbackArgs, SecondAjaxSuccessCallbackArgs){console.log('success');}, function(){console.log('failed');});
如果有一个失败了都会执行失败的回调。
将我们上面并行化操作的代码改良后:
$.when($.get('api1/data'),$.get('api2/data'),$.get('api3/data'),{ key: 'value' }
).done();
5.promse
方法是返回的一个promise
对象,该对象只能添加回调或者查看状态,但不能触发。我们通常将该方法暴露给外层使用,而内部应该使用deferred
来触发回调。
如何使用deferred
封装异步函数
第一种:
function getData(){// 1) create the jQuery Deferred object that will be usedvar deferred = $.Deferred();// ---- AJAX Call ---- //var xhr = new XMLHttpRequest();xhr.open("GET","data",true);// register the event handlerxhr.addEventListener('load',function(){if(xhr.status === 200){// 3.1) RESOLVE the DEFERRED (this will trigger all the done()...)deferred.resolve(xhr.response);}else{// 3.2) REJECT the DEFERRED (this will trigger all the fail()...)deferred.reject("HTTP error: " + xhr.status);}},false) // perform the workxhr.send();// Note: could and should have used jQuery.ajax. // Note: jQuery.ajax return Promise, but it is always a good idea to wrap it// with application semantic in another Deferred/Promise // ---- /AJAX Call ---- //// 2) return the promise of this deferredreturn deferred.promise();
}
第二种方法:
function prepareInterface() { return $.Deferred(function( dfd ) { var latest = $( “.news, .reactions” ); latest.slideDown( 500, dfd.resolve ); latest.addClass( “active” ); }).promise();
}
Deferred
的一些使用技巧:
1.异步缓存
以ajax
请求为例,缓存机制需要确保我们的请求不管是否已经存在于缓存,只能被请求一次。 因此,为了缓存系统可以正确地处理请求,我们最终需要写出一些逻辑来跟踪绑定到给定url
上的回调。
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(requestFunc){var cache = {};return function(key, callback){if(!cache[key]) {cache[key] = $.Deferred(function(defer){requestFunc(defer, key);}).promise();}return cache[key].done(callback);};};// 现在具体的请求逻辑已经抽象出来,我们可以重新写cachedGetScript:$.cachedGetScript = $.createCache(function(defer, url){$.getScript(url).then(defer.resolve, defer.reject);});
我们可以使用这个通用的异步缓存很轻易的实现一些场景:
图片加载
// 确保我们不加载同一个图像两次$.loadImage = $.createCache(function(defer, url){var image = new Image();function clearUp(){image.onload = image.onerror = null;}defer.then(clearUp, clearUp);image.onload = function(){defer.resolve(url);};image.onerror = defer.reject;image.src = url;});// 无论image.png是否已经被加载,或者正在加载过程中,缓存都会正常工作。$.loadImage( "my-image.png" ).done( callback1 ); $.loadImage( "my-image.png" ).done( callback1 );
缓存响应数据
$.searchTwitter = $.createCache(function(defer, query){$.ajax({url: 'http://search.twitter.com/search.json',data: {q: query}, dataType: 'jsonp'}).then(defer.resolve, defer.reject);});// 在Twitter上进行搜索,同时缓存它们$.searchTwitter( "jQuery Deferred", callback1 );
定时,
基于deferred的缓存并不限定于网络请求;它也可以被用于定时目的。
// 新的afterDOMReady辅助方法用最少的计数器提供了domReady后的适当时机。 如果延迟已经过期,回调会被马上执行。$.afterDOMReady = (function(){var readyTime;$(function(){readyTime = (new Date()).getTime();});return $.createCache(function(defer, delay){delay = delay || 0;$(function(){var delta = (new Date()).getTime() - readyTime;if(delta >= delay) {defer.resolve();} else {setTimeout(defer.resolve, delay - delta);}});});})();
2.同步多个动画
var fadeLi1Out = $('ul > li').eq(0).animate({opacity: 0}, 1000);var fadeLi2In = $('ul > li').eq(1).animate({opacity: 1}, 2000);// 使用$.when()同步化不同的动画$.when(fadeLi1Out, fadeLi2In).done(function(){alert('done');});
虽然jQuery1.6
以上的版本已经把deferred包装到动画里了,但如果我们想要手动实现,也是一件很轻松的事:
$.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();
};// 我们也可以使用同样的技巧,建立了一些辅助方法:
$.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(); };
});
3.一次性事件
例如,您可能希望有一个按钮,当它第一次被点击时打开一个面板,面板打开之后,执行特定的初始化逻辑。 在处理这种情况时,通常会这样写代码:
var buttonClicked = false;
$( "#myButton" ).click(function() { if ( !buttonClicked ) { buttonClicked = true; initializeData(); showPanel(); }
});
这是一个非常耦合的解决办法。 如果你想添加一些其他的操作,你必须编辑绑定代码或拷贝一份。 如果你不这样做,你唯一的选择是测试buttonClicked
。由于buttonClicked
可能是false
,新的代码可能永远不会被执行,因此你 可能会失去这个新的动作。
使用deferreds
我们可以做的更好 (为简化起见,下面的代码将只适用于一个单一的元素和一个单一的事件类型,但它可以很容易地扩展为多个事件类型的集合):
$.fn.bindOnce = function(event, callback){var element = this;defer = element.data('bind_once_defer_' + event);if(!defer) {defer = $.Deferred();function deferCallback(){element.off(event, deferCallback);defer.resolveWith(this, arguments);}element.on(event, deferCallback);element.data('bind_once_defer_' + event, defer);}return defer.done(callback).promise();
};$.fn.firstClick = function( callback ) { return this.bindOnce( "click", callback ); }; var openPanel = $( "#myButton" ).firstClick();
openPanel.done( initializeData );
openPanel.done( showPanel );
该代码的工作原理如下:
· 检查该元素是否已经绑定了一个给定事件的deferred
对象
· 如果没有,创建它,使它在触发该事件的第一时间解决
· 然后在deferred
上绑定给定的回调并返回promise
4.多个组合使用
单独看以上每个例子,deferred
的作用是有限的 。 然而,deferred
真正的力量是把它们混合在一起。
*在第一次点击时加载面板内容并打开面板
假如,我们有一个按钮,可以打开一个面板,请求其内容然后淡入内容。使用我们前面定义的方法,我们可以这样做:
var panel = $('#myPanel');
panel.firstClick(function(){$.when($.get('panel.html'),panel.slideDown()).done(function(ajaxArgs){panel.html(ajaxArgs[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属性描述图片的真实路径。 那么使用deferred来解决该用例的代码如下:
*/
$('#myBtn').firstClick(function(){var panel = $('#myPanel');var promises = [];$('img', panel).each(function(){var image = $(this);var src = element.data('src');if(src) {promises.push($.loadImage(src).then(function(){image.attr('src', src);}, function(){image.attr('src', 'error.png');}));}});promises.push(panel.slideDown);$.when.apply(null, promises).done(function(){panel.fadeIn();});
});
*在特定延时后加载页面上的图像
假如,我们要在整个页面实现延迟图像显示。 要做到这一点,我们需要的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.data( "src" ), after = element.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.data( "data-src" ), after = element.data( "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(); }); } ); }
});
这里,我们首先在尝试加载图片之前等待延迟条件满足。当你想在页面加载时限制网络请求的数量会非常有意义。
Deferred
的使用场所:
Ajax(XMLHttpRequest)
Image Tag,Script Tag,iframe(原理类似)
setTimeout/setInterval
CSS3 Transition/Animation
HTML5 Web Database
postMessage
Web Workers
Web Sockets
and more…
jQuery中 $.done
和 $.always
有什么区别呢?
jQuery中Ajax有done和always这两个回调方法
done
:成功时执行,异常时不会执行。
always
:不论成功与否都会执行。
jQuery中的Deferred详解和使用相关推荐
- Java程序员从笨鸟到菜鸟之(八十九)跟我学jquery(五)jquery中的ajax详解
Ajax让用户页面丰富起来, 增强了用户体验. 使用Ajax是所有Web开发的必修课. 虽然Ajax技术并不复杂, 但是实现方式还是会因为每个开发人员的而有所差异.jQuery提供了一系列Ajax函数 ...
- 【转】4.1触碰jQuery:AJAX异步详解
传送门:异步编程系列目录-- 示例源码:触碰jQuery:AJAX异步详解.rar AJAX 全称 Asynchronous JavaScript and XML(异步的 JavaScript 和 X ...
- 请求对象触碰jQuery:AJAX异步详解
最近一直在研究请求对象之类的问题,现在正好有机会和大家讨论一下. 传送门:异步编程系列目录-- 示例源码:触碰jQuery:AJAX异步详解.rar AJAX 全称 Asynchronous Java ...
- js路由在php上面使用,React中路由使用详解
这次给大家带来React中路由使用详解,React中路由使用的注意事项有哪些,下面就是实战案例,一起来看一下. 路由 通过 URL 映射到对应的功能实现,React 的路由使用要先引入 react-r ...
- jQuery.extend 函数使用详解
jQuery.extend 函数使用详解 一.总结 一句话总结: jquery $.extend的作用就是:用于将一个或多个对象的内容合并到目标对象:$.extend( target [, objec ...
- jQuery Validate验证框架详解
2019独角兽企业重金招聘Python工程师标准>>> 一.导入js库 <script type="text/javascript" src="& ...
- jQuery 表单验证插件,jQuery Validation Engine用法详解
jQuery 表单验证插件,jQuery Validation Engine用法详解 功能强大的 jQuery 表单验证插件,适用于日常的 E-mail.电话号码.网址等验证及 Ajax 验证,除自身 ...
- atitit.jQuery Validate验证框架详解与ati Validate 设计新特性
atitit.jQuery Validate验证框架详解与ati Validate 设计新特性 1. AtiValidate的目标1 2. 默的认校验规则1 2.1. 使用方式 1.metadata用 ...
- 最全的jquery datatables api 使用详解
https://www.cnblogs.com/amoniyibeizi/p/4548111.html 最全的jquery datatables api 使用详解 学习可参考:http://www.g ...
最新文章
- android 无线调试
- mysql联合索引的数据结构
- Geomesa-Hbase单机部署及ingest、export shp文件数据
- 使用Qt正则表达式提取全路径的文件名
- 南科大计算机系实力a,五大竞赛学科A+高校排行榜发布!北大实力碾压,科大赶超清华...
- Mr.J -- yield关键字生成器产生值
- [文章备份]RSS阅读器在手机上APP配置方法
- Rendering Path
- 关于OpenGL ES中的纹理压缩
- 即时通讯学习笔记007---在windows下安装openfire_并且使用自定义的数据库这里用mysql
- WC3L联赛结束了!!!
- 在SPSS中将统计表格外观修改为三线表外观的步骤
- nginx静态代理设置一:静态文件在本机
- 不用 qlv 格式转换成 mp4 - 优雅的下载腾讯视(mp4 格式)
- Zabbix拓扑图与聚合图形
- mysql 100w 查询耗时4秒_MySql百万数据0秒筛选查询
- 力扣每日一题第495题提莫攻击
- ECNA 2017 Problem J: Workout for a Dumbbell 模拟
- 电子封装行业市场专项调查分析
- GBase 8a MPP灾备方案