首先,为何要使用Deferred?javascript
先来看一段AJAX的代码:html
1 var data; 2 $.get('api/data', function(resp) { 3 data = resp.data; 4 }); 5 doSomethingFancyWithData(data);
这段代码极容易出问题,请求时间多长或者超时,将会致使咱们获取不到data。只有把请求设置为同步咱们才可以等待获取到data,才执行咱们的函数。可是这会带来阻塞,致使用户界面一直被冻结,对用户体验有很严重的影响。因此咱们须要使用异步编程,java
JS的异步编程有两种方式基于事件和基于回调,react
传统的异步编程会带来的一些问题,jquery
1.序列化异步操做致使的问题:web
1),延续传递风格Continuation Passing Style (CPS)ajax
2),深度嵌套编程
3),回调地狱json
2.并行异步操做的困难api
下面是一段序列化异步操做的代码:
1 // Demonstrates nesting, CPS, 'callback hell' 2 $.get('api1/data', function(resp1) { 3 // Next that depended on the first response. 4 $.get('api2/data', function(resp2) { 5 // Next request that depended on the second response. 6 $.get('api3/data', function(resp3) { 7 // Next request that depended on the third response. 8 $.get(); // ... you get the idea. 9 }); 10 }); 11 });
当回调愈来愈多,嵌套越深,代码可读性就会愈来愈差。若是注册了多个回调,那更是一场噩梦!
再看另外一段有关并行化异步操做的代码:
$.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:
如今有很多库已经实现了Deferred的操做,其中jQuery的Deferred就很是热门:
先过目一下Deferred的API:
jQuery的有关Deferred的API简介:
1 $.ajax('data/url') 2 .done(function(response, statusText, jqXHR){ 3 console.log(statusText); 4 }) 5 .fail(function(jqXHR, statusText, error){ 6 console.log(statusText); 7 }) 8 ,always(function(){ 9 console.log('I will always done.'); 10 });
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连续使用,此功能至关于顺序调用异步回调。
1 $.ajax({ 2 url: 't2.html', 3 dataType: 'html', 4 data: { 5 d: 4 6 } 7 }).then(function(){ 8 console.log('success'); 9 },function(){ 10 console.log('failed'); 11 }).then(function(){ 12 console.log('second'); 13 return $.ajax({ 14 url: 'jquery-1.9.1.js', 15 dataType: 'script' 16 }); 17 }, function(){ 18 console.log('second f'); 19 return $.ajax({ 20 url: 'jquery-1.9.1.js', 21 dataType: 'script' 22 }); 23 }).then(function(){ 24 console.log('success2'); 25 },function(){ 26 console.log('failed2'); 27 });
上面的代码,若是第一个对t2.html的请求成功输出success,就会执行second的ajax请求,接着针对该请求是成功仍是失败,执行success2或者failed2。
若是第一个失败输出failed,而后执行second f的ajax请求(注意和上面的不同),接着针对该请求是成功仍是失败,执行success2或者failed2。
理解这些对失败处理很重要。
将咱们上面序列化异步操做的代码使用then方法改造后,代码立马变得扁平化了,可读性也加强了:
1 var req1 = $.get('api1/data'); 2 var req2 = $.get('api2/data'); 3 var req3 = $.get('api3/data'); 4 5 req1.then(function(req1Data){ 6 return req2.done(otherFunc); 7 }).then(function(req2Data){ 8 return req3.done(otherFunc2); 9 }).then(function(req3Data){ 10 doneSomethingWithReq3(); 11 });
4.接着介绍$.when的方法使用,主要是对多个deferred对象进行并行化操做,当全部deferred对象都获得解决就执行后面添加的相应回调。
1 $.when( 2 $.ajax({ 3 4 url: 't2.html' 5 6 }), 7 $.ajax({ 8 url: 'jquery-1.9.1-study.js' 9 }) 10 ).then(function(FirstAjaxSuccessCallbackArgs, SecondAjaxSuccessCallbackArgs){ 11 console.log('success'); 12 }, function(){ 13 console.log('failed'); 14 });
若是有一个失败了都会执行失败的回调。
将咱们上面并行化操做的代码改良后:
1 $.when( 2 $.get('api1/data'), 3 $.get('api2/data'), 4 $.get('api3/data'), 5 { key: 'value' } 6 ).done();
5.promse方法是返回的一个promise对象,该对象只能添加回调或者查看状态,但不能触发。咱们一般将该方法暴露给外层使用,而内部应该使用deferred来触发回调。
如何使用deferred封装异步函数
第一种:
1 function getData(){ 2 // 1) create the jQuery Deferred object that will be used 3 var deferred = $.Deferred(); 4 // ---- AJAX Call ---- // 5 var xhr = new XMLHttpRequest(); 6 xhr.open("GET","data",true); 7 8 // register the event handler 9 xhr.addEventListener('load',function(){ 10 if(xhr.status === 200){ 11 // 3.1) RESOLVE the DEFERRED (this will trigger all the done()...) 12 deferred.resolve(xhr.response); 13 }else{ 14 // 3.2) REJECT the DEFERRED (this will trigger all the fail()...) 15 deferred.reject("HTTP error: " + xhr.status); 16 } 17 },false) 18 19 // perform the work 20 xhr.send(); 21 // Note: could and should have used jQuery.ajax. 22 // Note: jQuery.ajax return Promise, but it is always a good idea to wrap it 23 // with application semantic in another Deferred/Promise 24 // ---- /AJAX Call ---- // 25 26 // 2) return the promise of this deferred 27 return deferred.promise(); 28 }
第二种方法:
1 function prepareInterface() { 2 return $.Deferred(function( dfd ) { 3 var latest = $( “.news, .reactions” ); 4 latest.slideDown( 500, dfd.resolve ); 5 latest.addClass( “active” ); 6 }).promise(); 7 }
Deferred的一些使用技巧:
1.异步缓存
以ajax请求为例,缓存机制须要确保咱们的请求无论是否已经存在于缓存,只能被请求一次。 所以,为了缓存系统能够正确地处理请求,咱们最终须要写出一些逻辑来跟踪绑定到给定url上的回调。
1 var cachedScriptPromises = {}; 2 3 $.cachedGetScript = function(url, callback){ 4 if(!cachedScriptPromises[url]) { 5 cachedScriptPromises[url] = $.Deferred(function(defer){ 6 $.getScript(url).then(defer.resolve, defer.reject); 7 }).promise(); 8 } 9 10 return cachedScriptPromises[url].done(callback); 11 };
咱们为每个url缓存一个promise对象。 若是给定的url没有promise,咱们建立一个deferred,并发出请求。 若是它已经存在咱们只须要为它绑定回调。 该解决方案的一大优点是,它会透明地处理新的和缓存过的请求。 另外一个优势是一个基于deferred的缓存 会优雅地处理失败状况。 当promise以‘rejected’状态结束的话,咱们能够提供一个错误回调来测试:
$.cachedGetScript( url ).then( successCallback, errorCallback );
请记住:不管请求是否缓存过,上面的代码段都会正常运做!
通用异步缓存
为了使代码尽量的通用,咱们创建一个缓存工厂并抽象出实际须要执行的任务
1 $.createCache = function(requestFunc){ 2 var cache = {}; 3 4 return function(key, callback){ 5 if(!cache[key]) { 6 cache[key] = $.Deferred(function(defer){ 7 requestFunc(defer, key); 8 }).promise(); 9 } 10 11 return cache[key].done(callback); 12 }; 13 }; 14 15 16 // 如今具体的请求逻辑已经抽象出来,咱们能够从新写cachedGetScript: 17 $.cachedGetScript = $.createCache(function(defer, url){ 18 $.getScript(url).then(defer.resolve, defer.reject); 19 });
咱们可使用这个通用的异步缓存很轻易的实现一些场景:
图片加载
1 // 确保咱们不加载同一个图像两次 2 $.loadImage = $.createCache(function(defer, url){ 3 var image = new Image(); 4 function clearUp(){ 5 image.onload = image.onerror = null; 6 } 7 defer.then(clearUp, clearUp); 8 image.onload = function(){ 9 defer.resolve(url); 10 }; 11 image.onerror = defer.reject; 12 image.src = url; 13 }); 14 15 // 不管image.png是否已经被加载,或者正在加载过程当中,缓存都会正常工做。 16 $.loadImage( "my-image.png" ).done( callback1 ); 17 $.loadImage( "my-image.png" ).done( callback1 );
缓存响应数据
1 $.searchTwitter = $.createCache(function(defer, query){ 2 $.ajax({ 3 url: 'http://search.twitter.com/search.json', 4 data: {q: query}, 5 dataType: 'jsonp' 6 }).then(defer.resolve, defer.reject); 7 }); 8 9 // 在Twitter上进行搜索,同时缓存它们 10 $.searchTwitter( "jQuery Deferred", callback1 );
定时,
基于deferred的缓存并不限定于网络请求;它也能够被用于定时目的。
1 // 新的afterDOMReady辅助方法用最少的计数器提供了domReady后的适当时机。 若是延迟已通过期,回调会被立刻执行。 2 $.afterDOMReady = (function(){ 3 var readyTime; 4 5 $(function(){ 6 readyTime = (new Date()).getTime(); 7 }); 8 9 return $.createCache(function(defer, delay){ 10 delay = delay || 0; 11 12 $(function(){ 13 var delta = (new Date()).getTime() - readyTime; 14 15 if(delta >= delay) { 16 defer.resolve(); 17 } else { 18 setTimeout(defer.resolve, delay - delta); 19 } 20 }); 21 }); 22 })();
2.同步多个动画
1 var fadeLi1Out = $('ul > li').eq(0).animate({ 2 opacity: 0 3 }, 1000); 4 var fadeLi2In = $('ul > li').eq(1).animate({ 5 opacity: 1 6 }, 2000); 7 8 // 使用$.when()同步化不一样的动画 9 $.when(fadeLi1Out, fadeLi2In).done(function(){ 10 alert('done'); 11 });
虽然jQuery1.6以上的版本已经把deferred包装到动画里了,但若是咱们想要手动实现,也是一件很轻松的事:
1 $.fn.animatePromise = function( prop, speed, easing, callback ) { 2 var elements = this; 3 4 return $.Deferred(function( defer ) { 5 elements.animate( prop, speed, easing, function() { 6 defer.resolve(); 7 if ( callback ) { 8 callback.apply( this, arguments ); 9 } 10 }); 11 }).promise(); 12 }; 13 14 // 咱们也可使用一样的技巧,创建了一些辅助方法: 15 $.each([ "slideDown", "slideUp", "slideToggle", "fadeIn", "fadeOut", "fadeToggle" ], 16 function( _, name ) { 17 $.fn[ name + "Promise" ] = function( speed, easing, callback ) { 18 var elements = this; 19 return $.Deferred(function( defer ) { 20 elements[ name ]( speed, easing, function() { 21 defer.resolve(); 22 if ( callback ) { 23 callback.apply( this, arguments ); 24 } 25 }); 26 }).promise(); 27 }; 28 });
3.一次性事件
例如,您可能但愿有一个按钮,当它第一次被点击时打开一个面板,面板打开以后,执行特定的初始化逻辑。 在处理这种状况时,一般会这样写代码:
1 var buttonClicked = false; 2 $( "#myButton" ).click(function() { 3 if ( !buttonClicked ) { 4 buttonClicked = true; 5 initializeData(); 6 showPanel(); 7 } 8 });
这是一个很是耦合的解决办法。 若是你想添加一些其余的操做,你必须编辑绑定代码或拷贝一份。 若是你不这样作,你惟一的选择是测试buttonClicked。因为buttonClicked多是false,新的代码可能永远不会被执行,所以你 可能会失去这个新的动做。
使用deferreds咱们能够作的更好 (为简化起见,下面的代码将只适用于一个单一的元素和一个单一的事件类型,但它能够很容易地扩展为多个事件类型的集合):
1 $.fn.bindOnce = function(event, callback){ 2 var element = this; 3 defer = element.data('bind_once_defer_' + event); 4 5 if(!defer) { 6 defer = $.Deferred(); 7 8 function deferCallback(){ 9 element.off(event, deferCallback); 10 defer.resolveWith(this, arguments); 11 } 12 13 element.on(event, deferCallback); 14 element.data('bind_once_defer_' + event, defer); 15 } 16 17 return defer.done(callback).promise(); 18 }; 19 20 $.fn.firstClick = function( callback ) { 21 return this.bindOnce( "click", callback ); 22 }; 23 24 var openPanel = $( "#myButton" ).firstClick(); 25 openPanel.done( initializeData ); 26 openPanel.done( showPanel );
该代码的工做原理以下:
· 检查该元素是否已经绑定了一个给定事件的deferred对象
· 若是没有,建立它,使它在触发该事件的第一时间解决
· 而后在deferred上绑定给定的回调并返回promise
4.多个组合使用
单独看以上每一个例子,deferred的做用是有限的 。 然而,deferred真正的力量是把它们混合在一块儿。
*在第一次点击时加载面板内容并打开面板
假如,咱们有一个按钮,能够打开一个面板,请求其内容而后淡入内容。使用咱们前面定义的方法,咱们能够这样作:
1 var panel = $('#myPanel'); 2 panel.firstClick(function(){ 3 $.when( 4 $.get('panel.html'), 5 panel.slideDown() 6 ).done(function(ajaxArgs){ 7 panel.html(ajaxArgs[0]).fadeIn(); 8 }); 9 });
*在第一次点击时载入图像并打开面板
假如,咱们已经的面板有内容,但咱们只但愿当第一次单击按钮时加载图像而且当全部图像加载成功后淡入图像。HTML代码以下:
1 <div id="myPanel"> 2 <img data-src="image1.png" /> 3 <img data-src="image2.png" /> 4 <img data-src="image3.png" /> 5 <img data-src="image4.png" /> 6 </div> 7 8 /* 9 咱们使用data-src属性描述图片的真实路径。 那么使用deferred来解决该用例的代码以下: 10 */ 11 $('#myBtn').firstClick(function(){ 12 var panel = $('#myPanel'); 13 var promises = []; 14 15 $('img', panel).each(function(){ 16 var image = $(this); 17 var src = element.data('src'); 18 19 if(src) { 20 promises.push( 21 $.loadImage(src).then(function(){ 22 image.attr('src', src); 23 }, function(){ 24 image.attr('src', 'error.png'); 25 }) 26 ); 27 } 28 }); 29 30 promises.push(panel.slideDown); 31 32 $.when.apply(null, promises).done(function(){ 33 panel.fadeIn(); 34 }); 35 });
*在特定延时后加载页面上的图像
假如,咱们要在整个页面实现延迟图像显示。 要作到这一点,咱们须要的HTML的格式以下:
1 <img data-src="image1.png" data-after="1000" src="placeholder.png" /> 2 <img data-src="image2.png" data-after="1000" src="placeholder.png" /> 3 <img data-src="image1.png" src="placeholder.png" /> 4 <img data-src="image2.png" data-after="2000" src="placeholder.png" /> 5 6 /* 7 意思很是简单: 8 image1.png,第三个图像当即显示,一秒后第一个图像显示 9 image2.png 一秒钟后显示第二个图像,两秒钟后显示第四个图像 10 */ 11 12 $( "img" ).each(function() { 13 var element = $( this ), 14 src = element.data( "src" ), 15 after = element.data( "after" ); 16 if ( src ) { 17 $.when( 18 $.loadImage( src ), 19 $.afterDOMReady( after ) 20 ).then(function() { 21 element.attr( "src", src ); 22 }, function() { 23 element.attr( "src", "error.png" ); 24 } ).done(function() { 25 element.fadeIn(); 26 }); 27 } 28 }); 29 30 // 若是咱们想延迟加载的图像自己,代码会有所不一样: 31 $( "img" ).each(function() { 32 var element = $( this ), 33 src = element.data( "data-src" ), 34 after = element.data( "data-after" ); 35 if ( src ) { 36 $.afterDOMReady( after, function() { 37 $.loadImage( src ).then(function() { 38 element.attr( "src", src ); 39 }, function() { 40 element.attr( "src", "error.png" ); 41 } ).done(function() { 42 element.fadeIn(); 43 }); 44 } ); 45 } 46 });
这里,咱们首先在尝试加载图片以前等待延迟条件知足。当你想在页面加载时限制网络请求的数量会很是有意义。
Deferred的使用场所: