长期以来,JavaScript开发人员一直使用回调函数来执行多项任务。 一个非常常见的示例是在事件(例如click
或keypress
)触发时,通过addEventListener()
函数添加回调以执行各种操作。 回调函数很简单,可以在简单情况下完成工作。 不幸的是,当您的网页越来越复杂并且您需要并行或顺序执行许多异步操作时,它们将变得无法管理。
ECMAScript 2015(又名ECMAScript 6)引入了一种本地方法来应对这种情况:承诺。 如果您不知道什么是诺言,可以阅读文章JavaScript承诺概述 。 jQuery提供了并且仍然提供了自己的诺言,称为Deferred objects 。 在将承诺引入ECMAScript之前,它们就已引入jQuery多年。 在本文中,我将讨论什么是Deferred
对象以及它们试图解决的问题。
一个简短的历史
Deferred
对象是jQuery 1.5中引入的,它是一个可链接的实用程序,用于将多个回调注册到回调队列中,调用回调队列以及中继任何同步或异步函数的成功或失败状态。 从那时起,它一直是讨论的主题,一些批评和发展中的许多变化。 批评的例子包括“ 您错过了承诺的重点”和“ JavaScript的承诺”,以及为什么jQuery的实现被破坏了 。
Deferred
与Promise对象一起代表了Promise的jQuery实现。 在jQuery 1.x和2.x版本中, Deferred
对象遵循CommonJS Promises / A建议 。 该提议被用作Promise / A +提议的基础,该提议是基于本地承诺的。 如引言中所述,jQuery之所以不遵守Promises / A +提议,是因为它甚至在构想该提议之前就实现了承诺方式。
由于jQuery是先驱,并且由于向后兼容的问题,在纯JavaScript以及jQuery 1.x和2.x中使用诺言的方式有所不同。 此外,由于jQuery遵循不同的建议,因此该库与实现了承诺的其他库(例如Q库 )不兼容。
在即将到来的jQuery 3中 ,与本机承诺(在ECMAScript 2015中实现)的互操作性已得到改善 。 出于向后兼容的原因,main方法( then()
)的签名仍然有些不同,但是其行为与标准更加一致。
jQuery中的回调
为了理解为什么您可能需要使用Deferred
对象,让我们讨论一个示例。 使用jQuery时,通常使用其Ajax方法执行异步请求。 出于示例的原因,假设您正在开发一个将Ajax请求发送到GitHub API的网页。 您的目标是检索用户存储库的列表,找到最新更新的存储库,找到名称中带有字符串“ README.md”的第一个文件,最后检索该文件的内容。 基于此描述,每个Ajax请求都只能在上一步完成时开始。 换句话说,请求必须按顺序运行。
将此描述转换为伪代码(请注意,我没有使用真正的GitHub API),我们得到:
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);
});
});
});
如本例所示,使用回调,我们必须嵌套调用以按所需顺序执行Ajax请求。 这使代码的可读性降低。 您有许多嵌套的回调或必须同步的独立回调的情况通常称为“回调地狱”。
为了使它更好一点,您可以从我创建的匿名内联函数中提取命名函数。 但是,此更改无济于事,我们仍然处于回调地狱。 输入“ 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
对象非常灵活,并提供了满足您所有需求的方法。 可以通过调用jQuery.Deferred()
方法来创建它,如下所示:
var deferred = jQuery.Deferred();
或者,使用$
快捷方式:
var deferred = $.Deferred();
创建后, 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规范中,承诺被兑现或被兑现的承诺被解决。 但是,在jQuery的文档中,“解决”一词用于指ECMAScript规范称为实现状态的内容。
由于提供的方法很多,因此本文无法涵盖所有方法。 但是,在接下来的部分中,我将向您展示Deferred
和Promise
的使用示例。 在第一个示例中,我们将重写在“ jQuery中的回调”一节中检查的代码段,但是我们将使用这些对象,而不是使用回调。 在第二个示例中,我将阐明所讨论的生产者-消费者类比。
递延的Ajax请求
在本节中,我将展示如何使用Deferred
对象及其一些方法来提高“ jQuery回调”部分中开发的代码的可读性。 在深入研究之前,我们必须了解我们需要哪种可用的方法。
根据我们的要求和提供的方法列表,很明显,我们可以使用done()
或then()
方法来管理成功的案例。 由于你们中的许多人可能已经习惯了JavaScript的Promise
对象,因此在此示例中,我将采用then()
方法。 这两种方法之间的一个重要区别是then()
可以将作为参数接收的值转发给在then()
定义的其他then()
, done()
, fail()
或progress()
调用。
最终结果如下所示:
var username = 'testuser';
var fileToSearch = 'README.md';
$.getJSON('https://api.github.com/user/' + username + '/repositories')
.then(function(repositories) {
return repositories[0].name;
})
.then(function(lastUpdatedRepository) {
return $.getJSON('https://api.github.com/user/' + username + '/repository/' + lastUpdatedRepository + '/files');
})
.then(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;
}
}
return README;
})
.then(function(README) {
return $.getJSON('https://api.github.com/user/' + username + '/repository/' + lastUpdatedRepository + '/file/' + README + '/content');
})
.then(function(content) {
console.log(content);
});
如您所见,代码具有更高的可读性,因为我们能够按相同级别(在缩进方面)的小步骤破坏整个过程。
创建基于承诺的setTimeout函数
如您所知, setTimeout()是一个在给定时间后执行回调函数的函数。 这两个元素(回调函数和时间)都应作为参数提供。 假设您想在一秒钟后将消息记录到控制台。 通过使用setTimeout()
函数,可以使用以下代码实现此目标:
setTimeout(
function() {
console.log('I waited for 1 second!');
},
1000
);
如您所见,第一个参数是要执行的函数,第二个参数是要等待的毫秒数。 该功能已经运行了好几年,但是如果您需要在Deferred
链中引入延迟怎么办?
在下面的代码中,我将向您展示如何使用jQuery提供的Promise
对象来开发基于Promise
的setTimeout()
函数。 为此,我将使用Deferred
对象的promise()
方法。
最终结果如下所示:
function timeout(milliseconds) {
// Create a new Deferred object
var deferred = $.Deferred();
// Resolve the Deferred after the amount of time specified by milliseconds
setTimeout(deferred.resolve, milliseconds);
// Return the Deferred's Promise object
return deferred.promise();
}
timeout(1000).then(function() {
console.log('I waited for 1 second!');
});
在此清单中,我定义了一个名为timeout()
的函数,该函数包装了JavaScript的本机setTimeout()
函数。 在timeout()
内部,我创建了一个新的Deferred
对象来管理异步任务,该任务包括在指定的毫秒数后解析Deferred
对象。 在这种情况下, timeout()
函数是值的生产者,因此它将创建Deferred
对象并返回Promise
对象。 这样,我确保函数的调用方(使用者)不能随意解析或拒绝Deferred
对象。 实际上,调用者只能使用诸如done()
和fail()
方法添加要执行的函数。
jQuery 1.x / 2.x和jQuery 3之间的区别
在使用Deferred
的第一个示例中,我们开发了一个片段,该片段寻找名称中包含字符串“ README.md”的文件,但是我们没有考虑找不到该文件的情况。 这种情况可以视为失败。 发生这种情况时,我们可能想中断通话链,直接跳到结尾。 为此,就像使用JavaScript的catch()
方法一样,自然会引发异常并使用fail()
方法捕获它。
在Promises / A和Promises / A +兼容库(例如jQuery 3.x)中,引发的异常被转换为拒绝,并调用失败回调,例如添加了fail()
的失败回调。 这将异常作为参数接收。
在jQuery 1.x和2.x中,未捕获的异常将暂停程序的执行。 这些版本允许抛出的异常冒泡,通常到达window.onerror
。 如果未定义任何函数来处理此异常,则会显示该异常的消息,并中止程序的执行。
为了更好地理解不同的行为,请看一下我的书中的以下示例:
var deferred = $.Deferred();
deferred
.then(function() {
throw new Error('An error message');
})
.then(
function() {
console.log('First success function');
},
function() {
console.log('First failure function');
}
)
.then(
function() {
console.log('Second success function');
},
function() {
console.log('Second failure function');
}
);
deferred.resolve();
在jQuery 3.x中,此代码会将消息“第一个失败函数”和“第二个成功函数”写入控制台。 原因是,正如我之前提到的,规范指出应将抛出的异常转换为拒绝,并且必须使用该异常调用失败回调。 另外,一旦异常被管理(在我们的示例中,失败回调传递给第二个then()
),就应该执行以下成功函数(在这种情况下,成功回调传递给第三个then()
)。
在jQuery 1.x和2.x中,仅执行第一个函数(抛出错误的函数),并且只在控制台上显示消息“ Uncaught Error:a error message”。
jQuery 1.x / 2.x
jQuery 3
为了进一步提高与ECMAScript 2015的兼容性,jQuery 3还向Deferred
和Promise
对象添加了一个名为catch()
的新方法。 这是一种定义处理程序的方法,该处理程序在Deferred
对象被rejected
或其Promise
对象处于拒绝状态时执行。 其签名如下:
deferred.catch(rejectedCallback)
此方法不过是then(null, rejectedCallback)
的快捷方式。
结论
在本文中,我向您介绍了jQuery的Promise实现。 Promise可以避免麻烦的技巧来同步并行异步函数,并且无需将回调嵌套在回调内部的回调中……
除了显示一些示例之外,我还介绍了jQuery 3如何通过本机Promise改进互操作性。 尽管旧版jQuery和ECMAScript 2015之间突出显示了差异,但Deferred
仍然是工具箱中功能强大的工具。 作为专业开发人员,并且随着项目难度的增加,您会发现自己经常使用它。
From: https://www.sitepoint.com/introduction-jquery-deferred-objects/