jQuery延迟对象
jQuery中的延迟对象代表一个工作单元,该工作单元稍后将完成,通常是异步完成的。工作单元完成后,可以将延迟的对象设置为已解析或者失败。
延迟的对象包含一个promise对象。通过promise对象,我们可以指定工作单元完成时将要发生的情况。我们可以通过在promise对象上设置回调函数来实现。在本文后面的部分中,我将向我们展示所有工作原理。
这是一个显示jQuery的延迟对象如何工作的图。如果我们现在还不了解,请不要担心。稍后将说明。阅读完说明后,我们可以返回到该图。
典型的延迟对象用例
延迟对象通常用于通常在某个异步工作单元完成时传递回调函数以执行的位置。当执行异步工作时,最常见的情况是:
- AJAX通话
- 通过网络加载资源
- setTimeout()或者setInterval()
- 动画制作
- 用户互动
当执行AJAX调用时,它将由浏览器异步执行。一旦AJAX调用完成,浏览器就会调用一些回调函数。
加载时使用JavaScript通过网络上的图像,我们可以指定在图像完全加载时调用的回调函数。本质上,加载图像与AJAX调用非常相似。两者都是通过网络发送的HTTP请求。
当我们使用JavaScript的setTimeout()和setInterval()函数时,会传递一些回调函数,以便在发生超时时执行。
动画通常运行一定的时间。动画结束时,我们可能需要执行一些操作,或者在动画之后进行清理,或者执行其他一些与动画相关的操作。例如,如果我们为删除列表中的一项设置动画效果,则可能希望通过实际从数据结构或者DOM中的列表中删除该项目来完成。
用户交互是另一种典型的异步情况。代码可能会打开一个对话框,并指定通过按"确定"按钮或者"取消"按钮关闭对话框时将发生的情况。有时,我们可以通过在"确定"和"取消"按钮上设置显式侦听器函数来解决这种异步情况,而不是将回调传递给打开对话框的函数。两者都是可行的选择。
使用jQuery的延迟对象
在展示如何使用延迟对象之前,让我首先向我们展示没有延迟对象的标准异步函数的外观。以下示例函数将回调函数作为参数,该参数在一定时间延迟后执行。
function doAsync(callback){ setTimeout(function() { callback(); }, 1000); }
用回调函数调用doAsync()函数看起来像这样:
doAsync(function() { console.log("Executed after a delay"); });
延迟后,将执行作为参数传递给doAsync()
的函数。
doAsync()
函数也可以使用jQuery延迟对象来实现。这是doAsync()
函数(称为doAsync2()
)的jQuery延迟对象示例实现:
function doAsync2() { var deferredObject = $.Deferred(); setTimeout(function() { deferredObject.resolve(); }, 1000); return deferredObject.promise(); }
首先,使用jQuery$ .Deferred()
函数创建一个延迟对象。延迟的对象本身不执行任何操作。它只是一些异步工作的表示。
其次,将一个函数传递给setTimeout()函数。延迟后将执行此函数。该函数调用deferredObject.resolve()
。这将导致延迟对象的内部状态更改为已解析(延迟之后)。
最后,返回与延迟对象关联的promise对象。通过promise对象,调用doAsync2()函数的代码可以决定如果延迟对象的状态变为已解决或者失败,应该怎么办。
调用doAsync2()函数如下所示:
var promise = doAsync2(); promise.done(function () { console.log("Executed after a delay"); });
注意如何不再将回调函数作为参数传递给doAsync2()函数。取而代之的是,将回调函数传递给promise对象上的done()
函数。一旦延迟对象的状态变为已解决,传递给done()
的回调函数将被执行。
如果延迟对象的状态变为失败,则传递给done()
的回调函数将不会执行。取而代之的是,传递给Promise对象的fail()
函数的回调函数将被执行。这是将回调函数传递给fail()
函数的方式:
var promise = doAsync2(); promise.done(function () { console.log("Executed after a delay"); }); promise.fail(function () { console.log("Executed if the async work fails"); });
此外,由于done()
和fail()
函数会返回promise对象本身,因此我们可以将以上代码缩短为:
doAsync2() .done(function () { console.log("Executed after a delay"); }).fail(function () { console.log("Executed if the async work fails"); });
当然,doAsync2()
函数的实现绝不会将延迟对象的状态设置为失败,因此在此示例中将永远不会执行该函数。让我们更改doAsync2()的实现(作为doAsync3()),以便延迟对象实际上可以将状态更改为失败。
function doAsync3() { var deferredObject = $.Deferred(); setTimeout(function() { var randomValue = Math.random(); if(randomValue < 0.5) { deferredObject.resolve(); } else { deferredObject.reject(); } }, 1000); return deferredObject.promise(); }
在此实现中,异步调用的函数会生成一个介于0和1之间的随机数。如果该数小于0.5,则将延迟对象的状态设置为已解析。否则,延迟对象的状态将设置为失败。这是通过调用延迟对象的reject()
函数来完成的。
延迟对象与承诺对象
如果我们查看jQuery文档,我们会看到一个延迟对象也具有done()
和fail()
函数,就像promise对象一样。这意味着,我们可以返回延迟的对象本身,而不是从延迟的对象返回promise对象。外观如下:
function doAsync4() { var deferredObject = $.Deferred(); setTimeout(function() { var randomValue = Math.random(); if(randomValue < 0.5) { deferredObject.resolve(); } else { deferredObject.reject(); } }, 1000); return deferredObject; }
doAsync4()和doAsync3()之间的唯一区别是最后一行。而不是返回deferredObject.promise()
,而是返回deferredObject
。
使用doAsync4()和doAsync3()的方式没有任何区别。我们也可以直接在延迟对象上调用done()
和fail()
。效果与在promise对象上调用这些函数相同。
返回延迟对象与返回承诺对象之间的主要区别在于,可以使用延迟对象来设置延迟对象的已解析状态或者失败状态。我们不能在promise对象上执行此操作。因此,如果我们不希望异步函数的用户能够修改延迟对象的状态,最好不要返回延迟对象,而只返回其promise对象(deferredObject.promise()
)。
promise对象
延迟对象返回的promise对象包含以下可以调用的函数:
- done()
- fail()
- always()
- progress()
- then()
- state()
我们已经看过的两个第一个函数,done()和fail()。当连接到承诺对象的延迟对象将其状态更改为已解决或者失败时,将调用它们。
" always()"函数也将回调函数作为参数。当延迟对象将状态更改为已解决或者失败时,始终会调用此回调函数。延期对象更改为哪个状态都没有关系。当状态改变时,首先调用传递给done()
或者fail()
的回调,然后调用传递给always()
的回调。
progress()函数可用于将进度回调函数添加到Promise对象。每当在连接到Promise对象的延迟对象上调用notify()
函数时,都会调用此回调函数。此系统可用于将长时间运行的异步操作的进度通知给侦听器。我将在本文后面的示例中向我们展示如何执行此操作的示例。
函数" then()"允许我们链接和过滤promise对象。我们也可以将其用作设置已解决和失败状态更改的回调函数的快捷方式,而不是使用done()
,fail()
和progress()
。我将在本文后面的部分中展示如何使用then()
函数。
state()函数返回连接到promise对象的延迟对象的状态。它将返回字符串" pending"," resolved"或者" rejected"之一。字符串" pending"表示尚未解析或者拒绝的延迟对象的状态。
将多个回调添加到一个Promise对象
我们可以为一个延迟对象的每个延迟对象状态添加多个回调函数。这是一个例子:
var promise = doAsync3(); promise.done(function() { console.log("done 1"); } ); promise.done(function() { console.log("done 2"); } ); promise.fail(function() { console.log("fail 1"); } ); promise.fail(function() { console.log("fail 2"); } ); promise.always(function() { console.log("always 1"); } ); promise.always(function() { console.log("always 2"); } );
这个例子使用done()
,fail()
和always()
函数分别添加两个回调。当延迟对象更改其状态时,将按注册它们的顺序调用回调函数。
解决或者拒绝延迟的对象
我们可以使用延迟对象的resolve()
和reject()
函数来解析或者拒绝延迟对象。这样做时,将调用通过延迟对象的Promise对象或者通过延迟对象本身添加的任何回调函数。我们实际上已经在前面看到了此作品的示例,但是我将在这里重复一下:
function doAsync3() { var deferredObject = $.Deferred(); setTimeout(function() { var randomValue = Math.random(); if(randomValue < 0.5) { deferredObject.resolve(); } else { deferredObject.reject(); } }, 1000); return deferredObject.promise(); }
如我们所见,延迟对象的resolve()
和reject()
函数是从传递给setTimeout()的延迟函数内部调用的。
实际上,我们可以将值传递给resolve()
和reject()
函数。然后,这些值将传递给通过done()
,fail()
和always()
函数注册的回调函数。
这是doAsync4()
重写的,以显示如何通过resolve()
和reject()
传递参数:
function doAsync4() { var deferredObject = $.Deferred(); setTimeout(function() { var randomValue = Math.random(); if(randomValue < 0.5) { deferredObject.resolve( randomValue, "val2" ); } else { deferredObject.reject( randomValue, "errorCode" ); } }, 1000); return deferredObject; }
注册回调函数时,应使回调函数将传递的值用作参数。这是一个例子:
doAsync4().done(function (value, value2) { console.log("doAsync4 succeeded: " + value + " : " + value2); }).fail(function(value, value2) { console.log("doAsync4 failed: " + value + " : " + value2); }).always(function(value, value2) { console.log("always4: " + value + " : " + value2); });
如我们所见,这三个回调函数都带有两个参数。
我们可以通过resolve()
和reject()
传递任意多个参数。
监视延迟对象的进度
如果异步过程需要很长时间才能完成,则可以监视该过程的进度。它要求异步进程向潜在的侦听器通知长时间运行的进程的进度。这可以通过延迟对象的" notify()"函数来完成。这是显示如何调用notify()
函数的示例:
function doAsync5() { var deferredObject = $.Deferred(); setTimeout(function() { deferredObject.notify(1); }, 1000); setTimeout(function() { deferredObject.notify(2); }, 2000); setTimeout(function() { deferredObject.notify(3); }, 3000); setTimeout(function() { deferredObject.resolve(); }, 4000); return deferredObject.promise(); }
注意doAsync5()函数是如何设置四个延迟函数的。前三个延迟函数使用不同的参数值调用deferredObject.notify()
。最终的延迟函数解析延迟的对象。
在延迟对象上调用notify()
会导致调用通过关联的Promise对象上的progress()
函数添加的回调。这是在promise对象上使用progress()
函数的方式,以便在延迟对象上收到notify()
通知时得到通知:
doAsync5().progress(function(progressValue) { console.log("doAsync5 progress : " + progressValue) }).done(function () { console.log("doAsync5 succeeded. "); });
注意,对由doAsync5()返回的promise对象进行的" progress()"调用。每当在连接到Promise对象的延迟对象上调用notify()
函数时,都将调用传递给progress()
的回调。无论我们传递给notify()
的参数是什么,都将转发给在progress()
中注册的回调函数。
链接和过滤承诺对象
Promise对象具有称为then()
的函数,可用于链接Promise对象。这是一个使用then()
的链接允诺示例:
doAsync5().then(function(val) { console.log("done 1") }, function(val) { console.log("fail 1") }, function(val) { console.log("prog 1") } ).then(function(val) { console.log("done 2") }, function(val) { console.log("fail 2") }, function(val) { console.log("prog 2") } );
then()函数具有三个参数。第一个参数是在解析(完成)延迟对象时要调用的函数。第二个函数是如果延迟的对象被拒绝(失败)时调用的函数。第三个函数是一个进度函数,每当异步代码在延迟对象上调用notify()
时,都会调用该函数。
上面示例中的代码类似于以下代码:
doAsync5() .done(function(val) { console.log("done 1") }) .fail(function(val) { console.log("fail 1") }) .progress(function(val) { console.log("prog 1") }) .done(function(val) { console.log("done 2") }) .fail(function(val) { console.log("fail 2") }) .progress(function(val) { console.log("prog 2") }) ;
这两个版本都设置了两组侦听器函数,以侦听延迟的对象状态更改(已解决/已拒绝)和进度通知。但是有一个明显的区别。
在最后一个使用done()
,fail()
和progress()
的版本中,传递给每个侦听器的值是传递给resolve()
,reject()
和notify的确切值。 ()
延后的对象。
在使用then()
的第一个版本中,每个侦听器函数都可以选择更改(过滤)传递给晚于自身注册的侦听器的值。它通过返回另一个值来实现。侦听器返回的值将是返回给该类型的下一个侦听器的值。因此,解析的侦听器可以过滤传递给后续解析的侦听器的值。进度侦听器可以过滤传递给后续进度侦听器的值。对于拒绝(失败)的侦听器也是如此。看这个例子:
doAsync5().then(function(val) { console.log("done 1: " + val); return val + 1; }, function(val) { console.log("fail 1: " + val) }, function(val) { console.log("prog 1: " + val); return val + 1; } ).then(function(val) { console.log("done 2: " + val) }, function(val) { console.log("fail 2: " + val) }, function(val) { console.log("prog 2: " + val) } )
在这个例子中,第一个done
和progress
侦听器返回传递给他们的原始值,并增加1(val ++
)。返回的递增值将成为传递给下一个同类侦听器函数的参数值。此过滤效果可用于创建高级的Promise侦听器链。
将承诺与$ .when()组合
如果需要等待一个以上的延迟对象完成,则可以使用$ .when()函数将它们的promise对象组合在一起。这是一个例子:
$.when(doAsync4(), doAsync4()) .done(function(val1, val2) { console.log("val1: " + val1); console.log("val2: " + val2); }) .fail(function(val1, val2) { console.log("fail: " + val1 + " : " + val2 + " : " + val3); }) ;
这个例子调用了两次doAsync4()函数,并使用$ .when()函数组合了两个函数调用返回的Promise对象。
$ .when()函数返回一个新的Promise对象,我们可以其中使用done()
,fail()
,always()
等添加回调函数。如果所有的Promise作为参数传递给$ .when()
调用成功(=在其延迟的对象上调用了resolve()
),然后将调用由$ .when()返回的promise通过done()
设置的回调。如果传递给$ .when()的一个promise对象失败,则将调用通过fail()添加的回调。因此,$ .when()函数的函数类似于逻辑上的and函数。所有输入的Promise必须成功,否则$ .when()返回的Promise也必须成功。
在$ .when()返回的promise中注册的回调函数的参数值与普通promise对象的参数在语义上略有不同。在普通的Promise对象上,传递给done()
回调的参数是传递给延迟对象的resolve()
函数的参数。但是,当我们将两个Promise对象组合在一起时,done()
侦听器会接收传递给两个延迟对象的resolve()
调用的参数。
组合resolve()
参数时,要考虑两种情况。第一种情况是所有对resolve()
的调用都取一个值。在这种情况下,传递给resolve()
调用的每个值将作为一个单独的参数传递给通过$ .when()函数返回的promise对象上的done()
函数注册的回调。这是在这种情况下访问resolve()
参数的示例:
$.when(doAsync4(), doAsync4()) .done(function(val1, val2) { console.log("val1: " + val1); console.log("val2: " + val2); }) .fail(function(val1, val2) { console.log("fail: " + val1 + " : " + val2 + " : " + val3); }) ;
val1参数将从传递给$ .when()的第一个延迟对象的resolve()调用中接收值。 val2参数将从传递给$ .when()的第二个延迟对象的resolve()调用中接收值。
万一resolve()
调用中的一个或者两个都接收到多个参数值,那么上面示例中的val1
和val2
的含义就会改变。来自第一个和第二个" resolve()"调用的参数值数组变成了数组,而不是参数值本身。在这种情况下,我们可以像这样访问参数值:
$.when(doAsync4(), doAsync4()) .done(function(val1, val2) { for(var i=0; i < val1.length; i++) { console.log("val1[" + i +"]: " + val1[i]); } for(var i=0; i < val2.length; i++) { console.log("val2[" + i +"]: " + val2[i]); } }) .fail(function(val1, val2) { console.log("fail: " + val1 + " : " + val2 + " : " + val3); }) ;
注意val1和val2现在都是数组。
万一延迟对象失败,则失败的延迟对象的reject()
调用的参数将传递给在$ .when()返回的promise对象上使用fail()
注册的回调中。无论一个或者多个延迟对象是否失败,我们都只能从第一个失败的延迟对象的reject()
调用中获取参数,作为对$ .when()返回的诺言中以fail()
注册的回调的参数。
递延对象相对于回调函数的优势
与将回调函数作为参数传递给异步函数相比,延迟对象具有一些优势。
与仅使用单个回调函数的函数相比,添加多个回调函数的能力是延迟对象和Promise对象所提供的优点之一。当然,一个异步函数可以只使用一个回调函数数组,但是要使promise对象发挥作用,每个异步函数都必须采用一个"完成"回调数组和一个"失败"回调数组,以及一个"始终"回调的数组。必须采用三个这样的数组并调用这些回调将导致笨拙的函数实现。将函数聚集在延迟对象及其诺言对象中的一个位置上要干净得多。
除了内部清除异步函数外,延迟对象还清除调用异步函数的外部代码。例如,$ .when()函数可以将两个或者多个promise很好地结合在一起,而不必在回调内部嵌套回调以等待一个以上的异步操作完成。
第三,一旦理解了延迟对象和promise对象,我们就几乎了解了使用这些构造的所有函数。我们了解如何处理返回值,如何处理故障以及在异步函数提供的情况下监视进度等。
jQuery中的延迟对象
jQuery使用延迟对象,并在其某些API中内部承诺。例如,当我们调用$ .ajax()函数时,它将返回一个jqXHR对象。在jqXHR对象上可用的函数中有三个承诺函数,分别是done(),fail()和always()。这意味着,即使jqXHR
对象不仅仅是一个Promise对象,我们也可以将其用作Promise对象。
Promise对象也可以与jQuery中的动画结合使用。以下示例是示例jQuery文档的修改版本:
$( "#theButton" ).on( "click", function() { $( "div" ).each(function( i ) { $( this ).fadeIn().fadeOut( 1000 * ( i + 1 ) ); }); $( "div" ).promise().done(function() { $( "p" ).append( " Finished! " ); }); });
单击具有id'theButton'的按钮时,所有div
元素都会淡入。其次,从所有div
元素的集合中获取一个promise对象。当所有选定的div
元素的动画结束时,此promise对象将调用其done()
回调。从语义上讲有点棘手,从元素选择中获得的promise对象已绑定到它们的动画上,我将向我们授予这一点。