1、问题提出
JavaScript编程几乎总是伴随着异步操作,传统的异步操作会在操作完成之后,使用回调函数传回结果,而回调函数中则包含了后续的工作。这也是造成异步编程困难的主要原因:我们一直习惯于“线性”地编写代码逻辑,但是大量异步操作所带来的回调函数,会把我们的算法分解地支离破碎。此时我们不能用if来实现逻辑分支,也不能用while/for/do来实现循环,更不用提异步操作之间的组合、错误处理以及取消操作了。因此也诞生了如jQuery Deferred这样的辅助类库.
例:
alert(1) setTimeout(function(){ alert(2) },0) alert(3) //alert(1) //alert(3) //alert(2)
因为异步而导致流程不正确,或者说我们的应用在某个程度上依赖第三方api的数据,那么就会面临一个共同的问题:
我们无法获悉一个API响应的延迟时间,应用程序的其他部分可能会被阻塞,直到它返回 结果。例如:因为无无阻塞,代码在发送AJAX这个请求后会继续执行,那么后续的操作如果依赖这个数据就会出错了,所以这里就需要等待AJAX返回,才能执行后续操作。。。Deferreds 对这个问题提供了一个更好的解决方案,它是非阻塞的,并且与代码完全解耦 。
2、解决方案
大多情况下,promise作为一个模型,提供了一个在软件工程中描述延时(或将来)概念的解决方案。
背后的思想:不是执行一个方法然后阻塞应用程序等待结果返回,而是返回一个promise对象来满足未来值。
Promise/A只是一种规范,Deferred可以看作这种规范的具体实现。
3、规范实现
Promise/A提议’定义了一个’then‘方法来注册回调,当处理函数返回结果时回调会执行。
promise回调会在处于以下两种不同的状态下执行:
- resolved:在这种情况下,数据是可用
- rejected:在这种情况下,出现了错误,没有可用的值
'then'方法接受两个参数:一个用于promise得到了解决(resolved),另一个用于promise拒绝(rejected)。伪代码:
promise.then( function( futureValue ) { //成功 } , function() { //失败 } );
在某些情况下,我们需要获得多个返回结果后,再继续执行应用程序(例如,在用户可以选择他们感兴趣的选项前,显示一组动态的选项)。这种情况下,'when'方法可以用来解决所有的promise都满足后才能继续执行的场景。
when( promise1, promise2, ... ).then(function( futureValue1, futureValue2, ... ) { //当所有的异步对象都准备完毕后,才执行 });例:如果同时执行多个动画的时候,我们就需要对每一个动画执行完毕后的处理下一个动作,意味着我们就要监听所有动画的执行完毕后的回调。
使用promise和‘when’方式却可以很直截了当的表示: 一旦动画执行完成,就可以执行下一步任务。最终的结果是我们可以可以简单的用一个回调来解决多个动画执行结果的等待问题:
when( function(){ //执行动画1 return promise }, function(){ //执行动画2 return promise 2 } ).then(function(){ //当2个动画完成后,我们在自行后续的代码 });基本上可以用非阻塞的逻辑方式编写代码并异步执行。 而不是直接将回调传递给函数,这可能会导致紧耦合的接口,通过promise模式可以很容易区分同步和异步的概念。
4、jQuery的deferred实现
4.1 整体的API介绍:
jQuery.Deferred() |
一个构造函数,返回一个链式实用对象方法来注册多个回调,回调队列, 调用回调队列,并转达任何同步或异步函数的成功或失败状态。 |
deferred.always() |
当Deferred(延迟)对象解决或拒绝时,调用添加处理程序 |
deferred.done() |
当Deferred(延迟)对象解决时,调用添加处理程序 |
deferred.fail() |
当Deferred(延迟)对象拒绝时,调用添加处理程序。 |
deferred.isRejected() |
确定一个Deferred(延迟)对象是否已被拒绝。 |
deferred.isResolved() |
确定一个Deferred(延迟)对象是否已被解决。 |
deferred.notify() |
根据给定的 args参数 调用Deferred(延迟)对象上进行中的回调 (progressCallbacks) |
deferred.notifyWith() |
根据给定的上下文(context)和args递延调用Deferred(延迟)对象上进行中的回调(progressCallbacks ) |
deferred.pipe() |
实用的方法来过滤 and/or 链Deferreds。 |
deferred.progress() |
当Deferred(延迟)对象生成进度通知时,调用添加处理程 |
deferred.promise() |
返回Deferred(延迟)的Promise(承诺)对象。 |
deferred.reject() |
拒绝Deferred(延迟)对象,并根据给定的args参数调用任何失败回调函数(failCallbacks) |
deferred.rejectWith() |
拒绝Deferred(延迟)对象,并根据给定的 context和args参数调用任何失败回调函数(failCallbacks)。 |
deferred.resolve() |
解决Deferred(延迟)对象,并根据给定的args参数调用任何完成回调函数(doneCallbacks) |
deferred.resolveWith() |
解决Deferred(延迟)对象,并根据给定的 context和args参数调用任何完成回调函数(doneCallbacks) |
deferred.state() |
确定一个Deferred(延迟)对象的当前状态 |
deferred.then() |
当Deferred(延迟)对象解决,拒绝或仍在进行中时,调用添加处理程序。 |
jQuery.when() |
提供一种方法来执行一个或多个对象的回调函数, Deferred(延迟)对象通常表示异步事件。 |
.promise() |
返回一个 Promise 对象用来观察当某种类型的所有行动绑定到集合,排队与否还是已经完成 |
4.2 构建异步对象对比:
传统写法:利用回调函数进行阻塞
function aaa(callback){ setTimeout(function(){ callback( 5 ); },1000); } function done(value){ alert(value) } aaa(function(value){ done(value); })
jQuery的deferred写法
var defer = $.Deferred(); //构建异步对象 setTimeout(function(){ defer.resolve( 5 ); },1000); var filtered = defer.then(function( value ) { return value * 2; }); filtered.done(function( value ) { console.log('打印出值',value) });
结论:传统的代码逻辑,通过嵌套回调函数,等待异步发送成功的通知后,在执行下一步操作,如果有大量的嵌套回调,耦合度,维护性,扩展性?
defered对象的意义:
1:代码可读性,扁平化结构
2:让支离破碎的代码结构,继续保存线性的代码逻辑,也就是异步代码转为同步
3: 从抽线的角度,提供了一个抽象的非阻塞的解决方案(如 Ajax 请求的响应),它创建一个 “promise” 对象,其目的是在未来某个时间点返回一个响应。
deferreds 可以理解为表示需要长时间才能完成的耗时操作的一种方式,相比于阻塞式函数它们是异步的,而不是阻塞应用程序等待其完成然后返回结果。 deferred对 象会立即返回,然后你可以把回调函数绑定到deferred对象上,它们会在异步处理完成后被调用。
5、Defered()源码解析
jQuery.extend({ Deferred : function(){} when : function() )}
Defered()和when()都是对jQuery静态方法的一个扩展。
Defered()源码:
//显而易见Deferred是个工厂类,返回的是内部构建的deferred对象 Deferred: function(func) { //tuples 元素集 其实是把相同有共同特性的代码的给合并成一种结构,然后通过一次处理 //tuples 创建三个$.Callbacks对象,分别表示成功,失败,处理中三种状态 var tuples = [ // action, add listener, listener list, final state ["resolve", "done", jQuery.Callbacks("once memory"), "resolved"], ["reject", "fail", jQuery.Callbacks("once memory"), "rejected"], ["notify", "progress", jQuery.Callbacks("memory")] ], state = "pending", //创建了一个promise对象,具有state、always、then、primise方法 promise = { state: function() { return state; }, always: function() { deferred.done(arguments).fail(arguments); return this; }, /* 步骤:递归jQuery.Deferred 传递了func 链式调用了promise() 作用:构建一个新的deferred对象,返回受限的promise对象 给父deferred对象的[ done | fail | progress ]方法都增加一个过滤函数的方法 */ then: function( /* fnDone, fnFail, fnProgress */ ) { var fns = arguments; //分别为deferred的三个callbacklist添加回调函数,根据fn的是否是函数,分为两种情况 return jQuery.Deferred(function(newDefer) { jQuery.each(tuples, function(i, tuple) { var action = tuple[0], fn = jQuery.isFunction(fns[i]) && fns[i]; // deferred[ done | fail | progress ] for forwarding actions to newDefer //deferred其实就是根级父对象的引用,所以就嵌套再深, //其实都是调用了父对象deferred[ done | fail | progress 执行add罢了 deferred[tuple[1]](function() { var returned = fn && fn.apply(this, arguments); if (returned && jQuery.isFunction(returned.promise)) { returned.promise() .done(newDefer.resolve) .fail(newDefer.reject) .progress(newDefer.notify); } else { newDefer[action + "With"](this === promise ? newDefer.promise() : this, fn ? [returned] : arguments); } }); }); fns = null; }).promise(); }, // Get a promise for this deferred // If obj is provided, the promise aspect is added to the object //扩展primise对象生成最终的Deferred对象,返回该对象 promise: function(obj) { return obj != null ? jQuery.extend(obj, promise) : promise; } }, deferred = {}; // Keep pipe for back-compat promise.pipe = promise.then; // Add list-specific methods //分解tuples元素集 jQuery.each(tuples, function(i, tuple) { var list = tuple[2], stateString = tuple[3]; // promise[ done | fail | progress ] = list.add promise[tuple[1]] = list.add; // Handle state if (stateString) { list.add(function() { // state = [ resolved | rejected ] state = stateString; // [ reject_list | resolve_list ].disable; progress_list.lock }, tuples[i ^ 1][2].disable, tuples[2][2].lock); } // deferred[ resolve | reject | notify ] deferred[tuple[0]] = function() { deferred[tuple[0] + "With"](this === deferred ? promise : this, arguments); return this; }; deferred[tuple[0] + "With"] = list.fireWith; }); // Make the deferred a promise promise.promise(deferred); // Call given func if any if (func) { func.call(deferred, deferred); } // All done! return deferred; },
通过工厂方法Deferred构建的异步对象带的所有的方法,构造图如下图所示:
6、when()函数分析
应用说明:
$.when()是$.Deferred()的辅助方法。描述: 提供一种方法来执行一个或多个对象的回调函数,返回这些对象的延时(Deferred)对象。
$.Deferred()也是返回一个延迟对象,那么它们的区别是什么呢?$.Deferred()只能针对一个延迟对象做成功,失败,进行中的操作,而$.when()可以针对多个延迟对象做以上操作。
举例:
function a(){ var cb = $.Defered(); cb.resolve(); return cb; } function b(){ var cb = $.Defered(); cb.resolve(); return cb; } a().done(function(){alert("成功")});
需求是等a中的延迟对象触发,以及b中的延迟对象触发后,再弹出成功,因为这时需要处理两个延迟对象,所以通过$.Defered()无法实现。这时就需要用到$when()了。可以把最后那句代码改成:
$when(a(),b()) .done(function(){alert("成功");});
就可以实现上面的需求。这句代码的意思是,等a、b中的延迟对象都触发了resolve后,才会执行里面的回调方法,弹出成功。
还有一个用法:
$.when(a(),b()) .done(function(){alert("成功");}) .fail(function(){alert("失败");})
这段代码的意思是:只要a或b中的任何一个延迟对象触发了reject方法,就会弹出失败。
由上面示例可知:when方法中传入的都是延迟对象,而且这个延迟对象还都是有状态的(调用resolve是成功状态。调用reject是失败状态)。如果传入放入不是延迟对象,例如字符串:
$.when( {"testing",b()) .done( function() { alert("成功"); }) .fail(function(){alert("失败");});
那么when方法会忽略testing这个字符串,只会正对b里面的延迟对象进行处理。如果都是字符串,那么将都忽略,直接执行done添加的方法。而且done添加的回调方法中,可以接受这些字符串(argument[0]是第一个字符串,argument[1]是第二个字符串,以此类推),例如:
$.when( { testing: 123 } ).done( function(x) { alert(x.testing); } /* alerts "123" */ );
由上面的例子,联想到一种$.when()的用法:
需求:一个页面有两份不同的数据需要运用两个ajax请求同时把他们刷到数据库中。
解决方案:$.when(ajax1,ajax2).done(successfun,errorfun)函数可以完成两个请求同时发送,如果其中一个失败,则errorfun回调函数被调用,否则successfun回调函数被调用。
when函数源码分析:
// $.when()是$.Deferred()的辅助方法。 //描述: 提供一种方法来执行一个或多个对象的回调函数,返回这些对象的延时(Deferred)对象。 //$.Deferred()也是返回一个延迟对象,那么它们的区别是什么呢?$.Deferred()只能针对一个延迟对象做成功,失败,进行中的操作, //而$.when()可以针对多个延迟对象做以上操作。 when: function(subordinate /* , ..., subordinateN */ ) { var i = 0, resolveValues = core_slice.call(arguments),//将传进来的参数变成数组 length = resolveValues.length,//参数长度 // the count of uncompleted subordinates //参数超过1或者传进来的是延迟对象,就将参数长度赋值给remaining,否则,remaining等于0用于下一步判断 remaining = length !== 1 || (subordinate && jQuery.isFunction(subordinate.promise)) ? length : 0, // the master Deferred. If resolveValues consist of only a single Deferred, just use that. //如果remaining=1,说明传参subordinate就是一个延迟对象,赋值给defered; //否则,自己创建一个延迟对象赋值给defered; //when方法返回的就是这个延迟对象。 deferred = remaining === 1 ? subordinate : jQuery.Deferred(), // Update function for both resolve and progress values //判断所有的延迟对象触发resolve了没有,如果都触发了, //就执行when返回延迟对象的resolve,如果不是全部,则不做处理。 updateFunc = function(i, contexts, values) { return function(value) { contexts[i] = this; values[i] = arguments.length > 1 ? core_slice.call(arguments) : value; if (values === progressValues) { deferred.notifyWith(contexts, values); } else if (!(--remaining)) { //如果触发的是resolve,remaining就会减1,直到为0,就证明when中的所有延迟对象都执行了resolve。 //这时,就会执行when返回的延迟对象的resolveWith,而这将会触发defered通过done添加的方法。 deferred.resolveWith(contexts, values); } }; }, progressValues, progressContexts, resolveContexts; // add listeners to Deferred subordinates; treat others as resolved if (length > 1) { progressValues = new Array(length); progressContexts = new Array(length); resolveContexts = new Array(length); for (; i < length; i++) { //如果传入的第i个参数是defered对象则进入if if (resolveValues[i] && jQuery.isFunction(resolveValues[i].promise)) { //给传入的defered对象添加done,fail等事件 resolveValues[i].promise() //如果有一个延迟对象触发resolve,就执行updateFunc .done(updateFunc(i, resolveContexts, resolveValues)) //只要有一个延迟对象触发reject,就会执行when返回延迟对象的reject。 .fail(deferred.reject) //外部参数的fail回调添加内部defered对象的reject方法 .progress(updateFunc(i, progressContexts, progressValues)); } else { --remaining;//如果传入的不是defered对象,则参数数组的长度自减1 } } } // if we're not waiting on anything, resolve the master if (!remaining) { //如果remaining=0;说明没有传入defered对象(不传参或传入的是字符串的时候), //就将自创的defered对象的resolveWith方法返回。 deferred.resolveWith(resolveContexts, resolveValues); } return deferred.promise(); }关于其中的updateFun方法, https://www.cnblogs.com/chaojidan/p/4170697.html?utm_source=tuicool这篇博客举例分析比较具体,由于我关于这个的解释还有点迷糊,所以这里就不贴了,感兴趣的可以直接看原博客。