jQuery的Deferred对象概述

本文深入探讨了jQuery中Promises的应用,特别是Deferred对象的作用及其解决的问题。通过实例对比了使用传统回调函数与Promises处理异步操作的区别,展示了如何利用jQuery Promises简化复杂的异步流程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

很久以来,JavaScript 开发者们习惯用回调函数的方式来执行一些任务。最常见的例子就是利用 addEventListener() 函数来添加一个回调函数, 用来在指定的事件(如 click 或 keypress)被触发时,执行一系列的操作。回调函数简单有效——在逻辑并不复杂的时候。遗憾的是,一旦页面的复杂度增加,而你因此需要执行很多并行或串行的异步操作时,这些回调函数会让你的代码难以维护。

ECMAScript 2015(又名 ECMAScript 6) 引入了一个原生的方法来解决这类问题:promises。如果你还不清楚 promise 是什么,可以阅读这篇文章《Javascript Promise概述》。jQuery 则提供了独具一格的另一种 promises,叫做 Deferred 对象。而且 Deferred 对象的引入时间要比 ECMAScript 引入 promise 早了好几年。在这篇文章里,我会介绍 Deferred 对象和它试图解决的问题是什么。
Deferred对象简史

Deferred 对象是在 jQuery 1.5 中引入的,该对象提供了一系列的方法,可以将多个回调函数注册进一个回调队列里、调用回调队列,以及将同步或异步函数执行结果的成功还是失败传递给对应的处理函数。从那以后,Deferred 对象就成了讨论的话题, 其中不乏批评意见,这些观点也一直在变化。一些典型的批评的观点如《你并没有理解 Promise 》和《论 Javascript 中的 Promise 以及 jQuery 是如何把它搞砸的》。

Promise 对象 是和 Deferred 对象一起作为 jQuery 对 Promise 的一种实现。在 jQuery1.x 和 2.x 版本中, Deferred 对象遵守的是《CommonJS Promises 提案》中的约定,而 ECMAScript 原生 promises 方法的建立基础《Promises/A+ 提案》也是以这一提案书为根基衍生而来。所以就像我们一开始提到的,之所以 Deferred 对象没有遵循《Promises/A+ 提案》,是因为那时后者根本还没被构想出来。

由于 jQuery 扮演的先驱者的角色以及后向兼容性问题,jQuery1.x 和 2.x 里 promises 的使用方式和原生 Javascript 的用法并不一致。此外,由于 jQuery 自己在 promises 方面遵循了另外一套提案,这导致它无法兼容其他实现 promises 的库,比如 Q library。

不过即将到来的 jQuery 3 改进了 同原生 promises(在 ECMAScript2015 中实现)的互操作性。虽然为了向后兼容,Deferred 对象的主要方法之一(then())的方法签名仍然会有些不同,但行为方面它已经同 ECMAScript 2015 标准更加一致。
jQuery中的回调函数

举一个例子来理解为什么我们需要用到 Deferred 对象。使用 jQuery 时,经常会用到它的 ajax 方法执行异步的数据请求操作。我们不妨假设你在开发一个页面,它能够发送 ajax 请求给 GitHub API,目的是读取一个用户的 Repository 列表、定位到最近更新一个 Repository,然后找到第一个名为“README.md”的文件并获取该文件的内容。所以根据以上描述,每一个请求只有在前一步完成后才能开始。换言之,这些请求必须依次执行。

上面的描述可以转换成伪代码如下(注意我用的并不是真正的 Github API):
  1. var username = 'testuser';
  2. var fileToSearch = 'README.md';

  3. $.getJSON('https://api.github.com/user/' + username + '/repositories', function(repositories) {
  4.   var lastUpdatedRepository = repositories[0].name;

  5. $.getJSON('https://api.github.com/user/' + username + '/repository/' + lastUpdatedRepository + '/files', function(files) {
  6.     var README = null;

  7. for (var i = 0; i < files.length; i++) {
  8.       if (files[i].name.indexOf(fileToSearch) >= 0) {
  9.         README = files[i].path;

  10. break;
  11.       }
  12.     }

  13. $.getJSON('https://api.github.com/user/' + username + '/repository/' + lastUpdatedRepository + '/file/' + README + '/content', function(content) {
  14.       console.log('The content of the file is: ' + content);
  15.     });
  16.   });
  17. });
复制代码
如你所见,使用回调函数的话,我们需要反复嵌套来让 ajax 请求按照我们希望的顺序执行。当代码里出现许多嵌套的回调函数,或者有很多彼此独立但需要将它们同步的回调时,我们往往把这种情形称作“回调地狱 ( callback hell )“。

为了稍微改善一下,你可以从我创建的匿名函数中提取出命名函数。但这帮助并不大,因为我们还是在回调的地狱中,依旧面对着回调嵌套和同步的难题。这时是 Deferred 和 Promise 对象上场的时候了。
Deferred和Promise对象

Deferred 对象可以被用来执行异步操作,例如 Ajax 请求和动画的实现。在 jQuery 中,Promise对象是只能由Deferred对象或 jQuery 对象创建。它拥有 Deferred 对象的一部分方法:always(),done(), fail(), state()和then()。我们在下一节会讲到这些方法和其他细节。

如果你来自于原生 Javascript 的世界,你可能会对这两个对象的存在感到迷惑:为什么 jQuery 有两个对象(Deferred 和 Promise)而原生JS 只有一个(Promise)? 在我著作的书《jQuery 实践(第三版)》里有一个类比,可以用来解释这个问题。

Deferred 对象通常用在从异步操作返回结果的函数里(返回结果可能是 error,也可能为空)——即结果的生产者函数里。而返回结果后,你不想让读取结果的函数改变 Deferred 对象的状态(译者注:包括 Resolved 解析态,Rejected 拒绝态),这时就会用到 promise 对象——即 Promise 对象总在异步操作结果的消费者函数里被使用。

为了理清这个概念,我们假设你需要实现一个基于 promise 的timeout()函数(在本文稍后会展示这个例子的代码)。你的函数会等待指定的一段时间后返回(这里没有返回值),即一个生产者函数。而这个函数的对应消费者们并不在乎操作的结果是成功(解析态 resolved)还是失败(拒绝态 rejected),而只关心他们需要在 Deferred 对象的操作成功、失败,或者收到进展通知后紧接着执行一些其他函数。此外,你还希望能确保消费者函数不会自行解析或拒绝 Deferred对象。为了达到这一目标,你必须在生产者函数timeout()中创建 Deferred 对象,并只返回它的 Promise 对象,而不是 Deferred对象本身。这样一来,除了timeout()函数之外就没有人能够调用到resolve()和reject()进而改变 Deferred 对象的状态了。

在这个 StackOverflow 问题 里你可以了解到更多关于 jQuery 中 Deferred 和 Promise 对象的不同。

既然你已经了解里这两个对象,让我们来看一下它们都包含哪些方法。
Deferred对象的方法

Deferred 对象相当灵活并提供了你可能需要的所有方法,你可以通过调用 jQuery.Deferred() 像下面一样创建它:
  1. var deferred = jQuery.Deferred();
复制代码
或者,使用 $ 作为 jQuery 的简写:
  1. var deferred = $.Deferred();
复制代码
创建完 Deferred 对象后,就可以使用它的一系列方法。处了已经被废弃的 removed 方法外,它们是:

    always(callbacks[, callbacks, ..., callbacks]): 添加在该 Deferred 对象被解析或被拒绝时调用的处理函数
    done(callbacks[, callbacks, ..., callbacks]): 添加在该 Deferred 对象被解析时调用的处理函数
    fail(callbacks[, callbacks, ..., callbacks]): 添加在该 Deferred 对象被拒绝时调用的处理函数
    notify([argument, ..., argument]):调用 Deferred 对象上的 progressCallbacks 处理函数并传递制定的参数
    notifyWith(context[, argument, ..., argument]): 在制定的上下文中调用 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 对象并以指定的参数调用所有的 doneCallbackswith 处理函数。
    resolveWith(context[, argument, ..., argument]): 解析一个 Deferred 对象并在指定的上下文中以指定参数调用所有的doneCallbacks处理函数。
    state(): 返回当前 Deferred 对象的状态。
    then(resolvedCallback[, rejectedCallback[, progressCallback]]): 添加在该 Deferred 对象被解析、拒绝或收到进展通知时被调用的处理函数

从以上这写方法的描述中,我想突出强调一下 jQuery 文档和 ECMAScript 标准在术语上的不同。在 ECMAScript 中, 不论一个 promise 被完成 (fulfilled) 还是被拒绝 (rejected),我们都说它被解析 (resolved) 了。然而在 jQuery 的文档中,被解析这个词指的是 ECMAScript 标准中的完成 (fulfilled) 状态。

由于上面列出的方法太多, 这里无法一一详述。不过在下一节会有几个展示 Deferred 和 Promise 用法的示例。第一个例子中我们会利用Deferred 对象重写“ jQuery 的回调函数”这一节的代码。第二个例子里我会阐明之前讨论的生产者–消费者这个比喻。
利用 Deferred 依次执行 Ajax 请求

这一节我会利用Deferred对象和它提供的方法使“jQuery 的回调函数”这一节的代码更具有可读性。但在一头扎进代码之前,让我们先搞清楚一件事:在 Deferred 对象现有的方法中,我们需要的是哪些。

根据我们的需求及上文的方法列表,很明显我们既可以用 done() 也可以通过 then() 来处理操作成功的情况,考虑到很多人已经习惯了使用JS 的原生 Promise 对象,这个示例里我会用 then() 方法来实现。要注意 then() 和 done() 这两者之间的一个重要区别是 then() 能够把接收到的值通过参数传递给后续的 then(),done(),fail() 或 progress() 调用。

所以最后我们的代码应该像下面这样:
  1. var username = 'testuser';
  2. var fileToSearch = 'README.md';

  3. $.getJSON('https://api.github.com/user/' + username + '/repositories')
  4.   .then(function(repositories) {
  5.     return repositories[0].name;
  6.   })
  7.   .then(function(lastUpdatedRepository) {
  8.     return $.getJSON('https://api.github.com/user/' + username + '/repository/' + lastUpdatedRepository + '/files');
  9.   })
  10.   .then(function(files) {
  11.     var README = null;

  12. for (var i = 0; i < files.length; i++) {
  13.       if (files[i].name.indexOf(fileToSearch) >= 0) {
  14.         README = files[i].path;

  15. break;
  16.       }
  17.     }

  18. return README;
  19.   })
  20.   .then(function(README) {
  21.     return $.getJSON('https://api.github.com/user/' + username + '/repository/' + lastUpdatedRepository + '/file/' + README + '/content');
  22.   })
  23.   .then(function(content) {
  24.     console.log(content);
  25.   });
复制代码
如你所见,由于我们能够把整个操作拆分成同在一个缩进层级的各个步骤,这段代码的可读性已经显著提高了。
创建一个基于 Promise 的 setTimeout 函数

你可能已经知道 setTimeout() 函数可以在延迟一个给定的时间后执行某个回调函数,只要你把时间和回调函数作为参数传给它。假设你想要在一秒钟后在控制台打印一条日志信息,你可以用它这样写:
  1. setTimeout(
  2.   function() {
  3.     console.log('等待了1秒钟!');
  4.   },
  5.   1000
  6. );
复制代码
如你所见,setTimeout 的第一个参数是要执行的回调函数,第二个参数是以毫秒为单位的等待时间。这个函数数年以来运转良好,但如果现在你需要在 Deferred 对象的方法链中引入一段时间的延时该怎么做呢?

下面的代码展示了如何用 jQuery 提供的 Promise 对象创建一个基于 promise 的 setTimeout(). 为了达到我们的目的,这里用到了 Deferred对象的 promise() 方法。

代码如下:
  1. function timeout(milliseconds) {
  2.   //创建一个新Deferred
  3.   var deferred = $.Deferred();

  4. // 在指定的毫秒数之后解析Deferred对象
  5.   setTimeout(deferred.resolve, milliseconds);

  6. // 返回Deferred对象的Promise对象
  7.   return deferred.promise();
  8. }

  9. timeout(1000).then(function() {
  10.   console.log('等待了1秒钟!');
  11. });
复制代码
这段代码里定义了一个名为 timeout() 的函数,它包裹在 JS 原生的 setTimeout() 函数之外。

在 timeout() 里, 创建了一个 Deferred 对象来实现在延迟指定的毫秒数之后将 Deferred 对象解析(Resolve)的功能。这里 timeout() 函数是值的生产者,因此它负责创建 Deferred 对象并返回 Promise 对象。这样一来调用者(消费者)就不能再随意解析或拒绝 Deferred 对象。事实上,调用者只能通过 done() 和 fail() 这样的方法来增加值返回时要执行的函数。
jQuery 1.x/2.x同 jQuery3 的区别

在第一个例子里,我们使用 Deferred 对象来查找名字包含“README.md”的文件, 但并没有考虑文件找不到的情况。这种情形可以被看成是操作失败,而当操作失败时,我们可能需要中断调用链的执行并直接跳到程序结尾。很自然地,为了实现这个目的,我们应该在找不到文件时抛出一个异常,并用 fail() 函数来捕获它,就像 Javascriopt 的 catch() 的用法一样。

在遵守 Promises/A 和 Promises/A+ 的库里(例如jQuery 3.x),抛出的异常会被转换成一个拒绝操作 (rejection),进而通过 fail() 方法添加的失败条件回调函数会被执行,且抛出的异常会作为参数传给这些函数。

在 jQuery 1.x 和 2.x中, 没有被捕获的异常会中断程序的执行。这两个版本允许抛出的异常向上冒泡,一般最终会到达 window.onerror。而如果没有定义异常的处理程序,异常信息就会被显示,同时程序也会停止运行。

为了更好的理解这一行为上的区别,让我们看一下从我书里摘出来的这一段代码:
  1. var deferred = $.Deferred();
  2. deferred
  3.   .then(function() {
  4.     throw new Error('一条错误信息');
  5.   })
  6.   .then(
  7.     function() {
  8.       console.log('第一个成功条件函数');
  9.     },
  10.     function() {
  11.       console.log('第一个失败条件函数');
  12.     }
  13.   )
  14.   .then(
  15.     function() {
  16.       console.log('第二个成功条件函数');
  17.     },
  18.     function() {
  19.       console.log('第二个失败条件函数');
  20.     }
  21.   );

  22. deferred.resolve();
复制代码
jQuery 3.x 中, 这段代码会在控制台输出“第一个失败条件函数” 和 “第二个成功条件函数”。原因就像我前面提到的,抛出异常后的状态会被转换成拒绝操作进而失败条件回调函数一定会被执行。此外,一旦异常被处理(在这个例子里被失败条件回调函数传给了第二个then()),后面的成功条件函数就会被执行(这里是第三个 then() 里的成功条件函数)。

在 jQuery 1.x 和 2.x 中,除了第一个函数(抛出错误异常的那个)之外没有其他函数会被执行,所以你只会在控制台里看到“未处理的异常:一条错误信息。”

你可以到下面两个JSBin链接中查看它们的执行结果的不同:

    jQuery 1.x/2.x
    jQuery 3

为了更好的改善它同 ECMAScript2015 的兼容性,jQuery3.x 还给 Deferred 和 Promise 对象增加了一个叫做 catch() 的新方法。它可以用来定义当 Deferred 对象被拒绝或 Promise 对象处于拒绝态时的处理函数。它的函数签名如下:
  1. deferred.catch(rejectedCallback)
复制代码
可以看出,这个方法不过是 then(null, rejectedCallback) 的一个快捷方式罢了。
总结

这篇文章里我介绍了 jQuery 实现的 promises。Promises 让我们能够摆脱那些用来同步异步函数的令人抓狂的技巧,同时避免我们陷入深层次的回调嵌套之中。

除了展示一些示例,我还介绍了 jQuery 3 在同原生 promises 互操作性上所做的改进。尽管我们强调了 jQuery 的老版本同ECMAScript2015 在 Promises 实现上有许多不同,Deferred 对象仍然是你工具箱里一件强有力的工具。作为一个职业开发人员,当项目的复杂度增加时,你会发现它总能派上用场。

转自解放号社区:http://bbs.jointforce.com/topic/26036

内容概要:本文深入探讨了Kotlin语言在函数式编程和跨平台开发方面的特性和优势,结合详细的代码案例,展示了Kotlin的核心技巧和应用场景。文章首先介绍了高阶函数和Lambda表达式的使用,解释了它们如何简化集合操作和回调函数处理。接着,详细讲解了Kotlin Multiplatform(KMP)的实现方式,包括共享模块的创建和平台特定模块的配置,展示了如何通过共享业务逻辑代码提高开发效率。最后,文章总结了Kotlin在Android开发、跨平台移动开发、后端开发和Web开发中的应用场景,并展望了其未来发展趋势,指出Kotlin将继续在函数式编程和跨平台开发领域不断完善和发展。; 适合人群:对函数式编程和跨平台开发感兴趣的开发者,尤其是有一定编程基础的Kotlin初学者和中级开发者。; 使用场景及目标:①理解Kotlin中高阶函数和Lambda表达式的使用方法及其在实际开发中的应用场景;②掌握Kotlin Multiplatform的实现方式,能够在多个平台上共享业务逻辑代码,提高开发效率;③了解Kotlin在不同开发领域的应用场景,为选择合适的技术栈提供参考。; 其他说明:本文不仅提供了理论知识,还结合了大量代码案例,帮助读者更好地理解和实践Kotlin的函数式编程特性和跨平台开发能力。建议读者在学习过程中动手实践代码案例,以加深理解和掌握。
内容概要:本文深入探讨了利用历史速度命令(HVC)增强仿射编队机动控制性能的方法。论文提出了HVC在仿射编队控制中的潜在价值,通过全面评估HVC对系统的影响,提出了易于测试的稳定性条件,并给出了延迟参数与跟踪误差关系的显式不等式。研究为两轮差动机器人(TWDRs)群提供了系统的协调编队机动控制方案,并通过9台TWDRs的仿真和实验验证了稳定性和综合性能改进。此外,文中还提供了详细的Python代码实现,涵盖仿射编队控制类、HVC增强、稳定性条件检查以及仿真实验。代码不仅实现了论文的核心思想,还扩展了邻居历史信息利用、动态拓扑优化和自适应控制等性能提升策略,更全面地反映了群体智能协作和性能优化思想。 适用人群:具备一定编程基础,对群体智能、机器人编队控制、时滞系统稳定性分析感兴趣的科研人员和工程师。 使用场景及目标:①理解HVC在仿射编队控制中的应用及其对系统性能的提升;②掌握仿射编队控制的具体实现方法,包括控制器设计、稳定性分析和仿真实验;③学习如何通过引入历史信息(如HVC)来优化群体智能系统的性能;④探索中性型时滞系统的稳定性条件及其在实际系统中的应用。 其他说明:此资源不仅提供了理论分析,还包括完整的Python代码实现,帮助读者从理论到实践全面掌握仿射编队控制技术。代码结构清晰,涵盖了从初始化配置、控制律设计到性能评估的各个环节,并提供了丰富的可视化工具,便于理解和分析系统性能。通过阅读和实践,读者可以深入了解HVC增强仿射编队控制的工作原理及其实际应用效果。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值