JavaScript开发者长期以来一直使用回调函数来执行多项任务。一个非常常见的例子是通过addEventListener()
函数添加回调,以便在触发点击或按键等事件时执行各种操作。回调函数简单易用,适用于简单场景。然而,当网页复杂度增加,需要并行或顺序执行许多异步操作时,回调函数就会变得难以管理。
ECMAScript 2015 (又名ECMAScript 6) 引入了一种处理此类情况的原生方法:Promise。如果您不了解Promise,可以阅读文章《JavaScript Promise概述》。jQuery 提供并仍然提供其自己的Promise版本,称为Deferred对象。在Promise被引入ECMAScript之前数年,Deferred对象就已经被引入jQuery。本文将讨论Deferred对象是什么,以及它们试图解决什么问题。
关键要点
resolve()
、reject()
、done()
、fail()
和then()
,允许开发者精细控制异步流程的处理。catch()
方法,与ECMAScript标准保持一致,并提供了一种简化的方法来处理Promise链中的错误。简史
Deferred对象在jQuery 1.5中引入,它是一个可链式调用的实用程序,用于将多个回调注册到回调队列中,调用回调队列,并传递任何同步或异步函数的成功或失败状态。从那时起,它就一直是讨论、一些批评和许多变化的主题。一些批评的例子包括《你错过了Promise的重点》和《JavaScript Promise以及为什么jQuery的实现是错误的》。
与Promise对象一起,Deferred代表了jQuery对Promise的实现。在jQuery 1.x和2.x版本中,Deferred对象遵循CommonJS Promises/A提案。该提案被用作Promises/A 提案的基础,而原生Promise就是基于此提案构建的。如引言中所述,jQuery不遵循Promises/A 提案的原因是,它在该提案提出之前很久就实现了Promise。
因为jQuery是先驱,并且由于向后兼容性问题,在纯JavaScript和jQuery 1.x和2.x中使用Promise的方式存在差异。此外,由于jQuery遵循不同的提案,该库与其他实现了Promise的库(如Q库)不兼容。
在即将推出的jQuery 3中,与原生Promise(如ECMAScript 2015中实现的)的互操作性得到了改进。出于向后兼容性的原因,主要方法(then()
)的签名仍然略有不同,但行为更符合标准。
jQuery中的回调函数
为了理解为什么您可能需要使用Deferred对象,让我们讨论一个例子。使用jQuery时,经常使用其Ajax方法执行异步请求。为了举例说明,假设您正在开发一个向GitHub API发送Ajax请求的网页。您的目标是检索用户的存储库列表,找到最近更新的存储库,找到名称中包含字符串“README.md”的第一个文件,最后检索该文件的内容。根据此描述,每个Ajax请求只有在上一步完成后才能开始。换句话说,请求必须按顺序运行。
将此描述转换为伪代码(请注意,我没有使用真实的GitHub API),我们得到:
<code class="language-javascript">var username = 'testuser'; var fileToSearch = 'README.md'; $.getJSON('https://api.github.com/user/' + username + '/repositories', function(repositories) { var lastUpdatedRepository = repositories[0].name; $.getJSON('https://api.github.com/user/' + username + '/repository/' + lastUpdatedRepository + '/files', function(files) { var README = null; for (var i = 0; i < files.length; i++) { if (files[i].name.indexOf(fileToSearch) >= 0) { README = files[i].path; break; } } $.getJSON('https://api.github.com/user/' + username + '/repository/' + lastUpdatedRepository + '/file/' + README + '/content', function(content) { console.log('The content of the file is: ' + content); }); }); });</code>
正如您在这个例子中看到的,使用回调函数,我们必须嵌套调用来按我们想要的顺序执行Ajax请求。这使得代码的可读性降低。有很多嵌套回调,或者必须同步的独立回调的情况,通常被称为“回调地狱”。
为了稍微改进一下,您可以从我创建的匿名内联函数中提取命名函数。但是,此更改并没有多大帮助,我们仍然发现自己处于回调地狱中。这时就需要Deferred和Promise对象了。
Deferred和Promise对象
在执行异步操作(例如Ajax请求和动画)时,可以使用Deferred对象。在jQuery中,Promise对象是从Deferred对象或jQuery对象创建的。它拥有Deferred对象方法的一个子集:always()
、done()
、fail()
、state()
和then()
。我将在下一节中介绍这些方法和其他方法。
如果您来自原生的JavaScript世界,您可能会对这两个对象的存在感到困惑。为什么当JavaScript只有一个(Promise)时会有两个对象(Deferred和Promise)?为了解释差异及其用例,我将采用我在我的书《jQuery in Action,第三版》中使用的相同比喻。
如果您编写处理异步操作并应返回值(也可以是错误或根本没有值)的函数,则通常使用Deferred对象。在这种情况下,您的函数是值的生产者,您希望阻止用户更改Deferred的状态。当您是函数的使用者时,使用Promise对象。
为了阐明这个概念,假设您想实现一个基于Promise的timeout()
函数(我将在本文的后续部分向您展示此例子的代码)。您负责编写必须等待给定时间量的函数(在这种情况下不返回值)。这使您成为生产者。您的函数的使用者不关心解析或拒绝它。使用者只需要能够添加函数以在Deferred的完成、失败或进度时执行。此外,您希望确保使用者无法自行解析或拒绝Deferred。为了实现此目标,您需要返回在timeout()
函数中创建的Deferred的Promise对象,而不是Deferred本身。通过这样做,您可以确保除了timeout()
函数之外,没有人可以调用resolve()
或reject()
方法。
您可以在此StackOverflow问题中阅读更多关于jQuery的Deferred和Promise对象之间区别的信息。
现在您知道了这些对象是什么,让我们来看看可用的方法。
Deferred方法
Deferred对象非常灵活,并提供满足您所有需求的方法。可以通过调用jQuery.Deferred()
方法创建它,如下所示:
<code class="language-javascript">var username = 'testuser'; var fileToSearch = 'README.md'; $.getJSON('https://api.github.com/user/' + username + '/repositories', function(repositories) { var lastUpdatedRepository = repositories[0].name; $.getJSON('https://api.github.com/user/' + username + '/repository/' + lastUpdatedRepository + '/files', function(files) { var README = null; for (var i = 0; i < files.length; i++) { if (files[i].name.indexOf(fileToSearch) >= 0) { README = files[i].path; break; } } $.getJSON('https://api.github.com/user/' + username + '/repository/' + lastUpdatedRepository + '/file/' + README + '/content', function(content) { console.log('The content of the file is: ' + content); }); }); });</code>
或者,使用$快捷方式:
<code class="language-javascript">var deferred = jQuery.Deferred();</code>
创建后,Deferred对象会公开多个方法。忽略那些已弃用或删除的方法,它们是:
always(callbacks[, callbacks, ..., callbacks])
:添加要在Deferred对象被解析或拒绝时调用的处理程序。done(callbacks[, callbacks, ..., callbacks])
:添加要在Deferred对象被解析时调用的处理程序。fail(callbacks[, callbacks, ..., callbacks])
:添加要在Deferred对象被拒绝时调用的处理程序。notify([argument, ..., argument])
:使用给定的参数调用Deferred对象的progressCallbacks
。notifyWith(context[, argument, ..., argument])
:使用给定的上下文和参数调用Deferred对象的progressCallbacks
。progress(callbacks[, callbacks, ..., callbacks])
:添加要在Deferred对象生成进度通知时调用的处理程序。promise([target])
:返回Deferred的Promise对象。reject([argument, ..., argument])
:拒绝Deferred对象并使用给定的参数调用任何failCallbacks
。rejectWith(context[, argument, ..., argument])
:拒绝Deferred对象并使用给定的上下文和参数调用任何failCallbacks
。resolve([argument, ..., argument])
:解析Deferred对象并使用给定的参数调用任何doneCallbacks
。resolveWith(context[, argument, ..., argument])
:解析Deferred对象并使用给定的上下文和参数调用任何doneCallbacks
。state()
:确定Deferred对象的当前状态。then(resolvedCallback[, rejectedCallback[, progressCallback]])
:添加要在Deferred对象被解析、拒绝或仍在进行中时调用的处理程序。这些方法的描述使我有机会强调jQuery文档和ECMAScript规范使用的术语之间的区别。在ECMAScript规范中,当Promise已完成或拒绝时,据说Promise已解析。然而,在jQuery的文档中,单词“resolved”用于指代ECMAScript规范称为“fulfilled”的状态。
由于提供的方法数量众多,本文无法涵盖所有方法。但是,在接下来的部分中,我将向您展示Deferred和Promise使用的几个示例。在第一个示例中,我们将重写“jQuery中的回调函数”部分中检查的代码片段,但我们将使用这些对象而不是回调函数。在第二个示例中,我将阐明讨论的生产者-消费者类比。
使用Deferred顺序执行Ajax请求
在本节中,我将展示如何使用Deferred对象及其一些方法来提高“jQuery中的回调函数”部分中开发的代码的可读性。在深入研究之前,我们必须了解我们需要哪些方法。
根据我们的需求和提供的方法列表,很明显我们可以使用done()
或then()
方法来管理成功的情况。由于你们中的许多人可能已经习惯了JavaScript的Promise对象,因此在这个例子中,我将使用then()
方法。这两个方法之间的一个重要区别是then()
能够将接收到的参数值转发到在其之后定义的其他then()
、done()
、fail()
或progress()
调用。
最终结果如下所示:
<code class="language-javascript">var username = 'testuser'; var fileToSearch = 'README.md'; $.getJSON('https://api.github.com/user/' + username + '/repositories', function(repositories) { var lastUpdatedRepository = repositories[0].name; $.getJSON('https://api.github.com/user/' + username + '/repository/' + lastUpdatedRepository + '/files', function(files) { var README = null; for (var i = 0; i < files.length; i++) { if (files[i].name.indexOf(fileToSearch) >= 0) { README = files[i].path; break; } } $.getJSON('https://api.github.com/user/' + username + '/repository/' + lastUpdatedRepository + '/file/' + README + '/content', function(content) { console.log('The content of the file is: ' + content); }); }); });</code>
如您所见,代码更易读,因为我们能够将整个过程分解成几个处于同一级别(关于缩进)的小步骤。
创建基于Promise的setTimeout函数
如您所知,setTimeout()
是一个在给定时间量后执行回调函数的函数。这两个元素(回调函数和时间)都应作为参数提供。假设您想在一秒钟后将消息记录到控制台。通过使用setTimeout()
函数,您可以使用下面显示的代码实现此目标:
<code class="language-javascript">var deferred = jQuery.Deferred();</code>
如您所见,第一个参数是要执行的函数,第二个参数是要等待的毫秒数。此函数多年来一直运行良好,但是如果您需要在Deferred链中引入延迟怎么办?
在下面的代码中,我将向您展示如何使用jQuery提供的Promise对象来开发基于Promise的setTimeout()
函数。为此,我将使用Deferred对象的promise()
方法。
最终结果如下所示:
<code class="language-javascript">var deferred = $.Deferred();</code>
在此列表中,我定义了一个名为timeout()
的函数,它包装了JavaScript的原生setTimeout()
函数。在timeout()
内部,我创建了一个新的Deferred对象来管理一个异步任务,该任务包括在指定的毫秒数后解析Deferred对象。在这种情况下,timeout()
函数是值的生产者,因此它创建Deferred对象并返回Promise对象。通过这样做,我确保函数的调用者(使用者)无法随意解析或拒绝Deferred对象。事实上,调用者只能使用done()
和fail()
等方法添加要执行的函数。
jQuery 1.x/2.x和jQuery 3之间的区别
在使用Deferred的第一个示例中,我们开发了一个查找名称中包含字符串“README.md”的文件的代码片段,但是我们没有考虑找不到此类文件的情况。这种情况可以看作是失败。当这种情况发生时,我们可能希望中断调用链并直接跳到其末尾。为此,自然会抛出异常并使用fail()
方法捕获它,就像使用JavaScript的catch()
方法一样。
在符合Promises/A和Promises/A 的库中(例如,jQuery 3.x),抛出的异常将转换为拒绝,并且将调用失败回调(例如使用fail()
添加的回调)。这将异常作为参数接收。
在jQuery 1.x和2.x中,未捕获的异常将停止程序的执行。这些版本允许抛出的异常冒泡,通常到达window.onerror
。如果未定义任何函数来处理此异常,则显示异常消息并中止程序的执行。
为了更好地理解不同的行为,请查看此示例:
<code class="language-javascript">var username = 'testuser'; var fileToSearch = 'README.md'; $.getJSON('https://api.github.com/user/' + username + '/repositories', function(repositories) { var lastUpdatedRepository = repositories[0].name; $.getJSON('https://api.github.com/user/' + username + '/repository/' + lastUpdatedRepository + '/files', function(files) { var README = null; for (var i = 0; i < files.length; i++) { if (files[i].name.indexOf(fileToSearch) >= 0) { README = files[i].path; break; } } $.getJSON('https://api.github.com/user/' + username + '/repository/' + lastUpdatedRepository + '/file/' + README + '/content', function(content) { console.log('The content of the file is: ' + content); }); }); });</code>
在jQuery 3.x中,此代码将消息“First failure function”和“Second success function”写入控制台。原因是,正如我之前提到的,规范指出抛出的异常应转换为拒绝,并且必须使用异常调用失败回调。此外,一旦异常被处理(在我们的示例中由传递给第二个then()
的失败回调处理),则应执行以下成功函数(在本例中是传递给第三个then()
的成功回调)。
在jQuery 1.x和2.x中,除了第一个函数(抛出错误的函数)之外,没有其他函数被执行,您只会看到控制台显示的消息“Uncaught Error: An error message”。
为了进一步提高其与ECMAScript 2015的兼容性,jQuery 3还在Deferred和Promise对象中添加了一个新方法catch()
。这是一种在Deferred对象被拒绝或其Promise对象处于拒绝状态时执行处理程序的方法。其签名如下:
<code class="language-javascript">var deferred = jQuery.Deferred();</code>
此方法只不过是then(null, rejectedCallback)
的快捷方式。
结论
在本文中,我向您介绍了jQuery对Promise的实现。Promise允许您避免使用讨厌的技巧来同步并行异步函数以及嵌套回调的需要……
除了展示一些示例之外,我还介绍了jQuery 3如何改进与原生Promise的互操作性。尽管突出了旧版jQuery和ECMAScript 2015之间的差异,但Deferred仍然是您工具箱中一个非常强大的工具。作为一名专业开发人员,随着项目难度的增加,您会发现自己经常使用它。
jQuery Deferred对象的常见问题解答 (FAQ)
(此处省略了FAQ部分,因为篇幅过长,且与文章主旨关系不大。如果需要,可以单独提出FAQ问题。)
以上是jQuery的介绍延期对象的详细内容。更多信息请关注PHP中文网其他相关文章!