jQuery延迟对象和承诺 - .then() vs .done()

521
我一直在阅读有关jQuery延迟对象(deferreds)和承诺(promises),但我无法看出在成功回调中使用.then().done()之间的区别。我知道Eric Hynds提到.done().success()映射到相同的功能,但我猜.then()也是如此,因为所有回调都在成功操作完成后被调用。
请问有人可以启示我正确的用法吗?

15
请注意,JQuery 3.0于2016年6月发布,是符合Promises/A+和ES2015 Promises规范的第一个版本。在此之前的实现与Promise所应提供的功能存在不兼容性。 - Flimm
我更新了我的回答,提供了更好的建议,告诉你什么时候使用。 - Robert Siemer
12个回答

600

done()附加的回调函数将在deferred对象被解决时触发。fail()附加的回调函数将在deferred对象被拒绝时触发。

在jQuery 1.8之前,then()仅仅是一种语法糖:

promise.then( doneCallback, failCallback )
// was equivalent to
promise.done( doneCallback ).fail( failCallback )

自1.8版本起,then()pipe()的别名,返回一个新的Promise对象。关于pipe()更多信息请参见这里

success()error()仅适用于调用ajax()方法返回的jqXHR对象。它们分别是done()fail()的简单别名:

jqXHR.done === jqXHR.success
jqXHR.fail === jqXHR.error

此外,done() 并不限于单个回调函数,还会过滤掉非函数的内容(尽管在版本1.8中存在字符串的错误,应该在1.8.1中修复):

// this will add fn1 to 7 to the deferred's internal callback list
// (true, 56 and "omg" will be ignored)
promise.done( fn1, fn2, true, [ fn3, [ fn4, 56, fn5 ], "omg", fn6 ], fn7 );

对于 fail() 同样适用。


10
回到一个新的Promise对象是我之前所忽略的关键点。我不明白为什么像$.get(....).done(function(data1) { return $.get(...) }).done(function(data2) { ... })这样的链式调用会导致data2未定义;当我将done改为then时,它就起作用了,因为我实际上想要将Promise对象串联在一起,而不是将更多的处理程序附加到原始Promise对象上。 - wrschneider
5
jQuery 3.0是第一个符合Promises/A+和ES2015规范的版本。 - Flimm
4
我仍然不明白为什么我会使用其中一个而不是另一个。如果我进行Ajax调用,并且需要等待该调用完全完成(即从服务器返回响应)后再调用另一个Ajax调用,我应该使用“done”还是“then”?为什么? - CodingYoshi
@CodingYoshi 请查看我的答案,最终回答了那个问题(使用.then())。 - Robert Siemer

441

在处理返回结果的方式上也有所不同(这被称为链式调用,done 不支持链式调用,而 then 则可以产生调用链)

promise.then(function (x) { // Suppose promise returns "abc"
    console.log(x);
    return 123;
}).then(function (x){
    console.log(x);
}).then(function (x){
    console.log(x)
})

以下的结果将被记录:

abc
123
undefined

promise.done(function (x) { // Suppose promise returns "abc"
    console.log(x);
    return 123;
}).done(function (x){
    console.log(x);
}).done(function (x){
    console.log(x)
})

将会得到以下内容:

abc
abc
abc

---------- 更新:

顺便提一下,如果您返回一个 Promise 而不是原子类型值,外部 Promise 将等待内部 Promise 解决:

promise.then(function (x) { // Suppose promise returns "abc"
    console.log(x);
    return $http.get('/some/data').then(function (result) {
        console.log(result); // suppose result === "xyz"
        return result;
    });
}).then(function (result){
    console.log(result); // result === xyz
}).then(function (und){
    console.log(und) // und === undefined, because of absence of return statement in above then
})

通过这种方式,编写并行或顺序异步操作变得非常简单,例如:

// Parallel http requests
promise.then(function (x) { // Suppose promise returns "abc"
    console.log(x);

    var promise1 = $http.get('/some/data?value=xyz').then(function (result) {
        console.log(result); // suppose result === "xyz"
        return result;
    });

    var promise2 = $http.get('/some/data?value=uvm').then(function (result) {
        console.log(result); // suppose result === "uvm"
        return result;
    });

    return promise1.then(function (result1) {
        return promise2.then(function (result2) {
           return { result1: result1, result2: result2; }
        });
    });
}).then(function (result){
    console.log(result); // result === { result1: 'xyz', result2: 'uvm' }
}).then(function (und){
    console.log(und) // und === undefined, because of absence of return statement in above then
})

上述代码并行发出了两个HTTP请求,从而使请求更快地完成,而下面的HTTP请求是按顺序运行的,从而减少了服务器负载。
// Sequential http requests
promise.then(function (x) { // Suppose promise returns "abc"
    console.log(x);

    return $http.get('/some/data?value=xyz').then(function (result1) {
        console.log(result1); // suppose result1 === "xyz"
        return $http.get('/some/data?value=uvm').then(function (result2) {
            console.log(result2); // suppose result2 === "uvm"
            return { result1: result1, result2: result2; };
        });
    });
}).then(function (result){
    console.log(result); // result === { result1: 'xyz', result2: 'uvm' }
}).then(function (und){
    console.log(und) // und === undefined, because of absence of return statement in above then
})

131
我赞同“done”对结果不产生任何影响,而“then”则会改变结果的观点。我认为其他人忽略了这一重要点。 - Shanimal
9
这里可能值得提一下适用的jQuery版本,因为then的行为在1.8版本中发生了变化。 - bradley.ayers
4
一针见血。如果有人想看混合使用donethen调用的链式结果,我创建了一个可运行示例 - Michael Kropat
7
上面的例子还表明了,'done' 方法作用于最初创建的原始 Promise 对象,而 'then' 方法则返回一个新的 Promise。 - Pulak Kanti Bhattacharyya
2
此外,jQuery 3.0是第一个符合ES2015和Promises/A+规范的版本。 - Flimm
显示剩余7条评论

60

.done()只有一个回调函数,即成功回调函数。

.then()有成功和失败两个回调函数。

.fail()只有一个失败回调函数。

所以你需要决定必须做什么...你在意它成功还是失败?


18
你没有提到“then”会产生调用链,可以参考Lu4的回答。 - oligofren
你的回答是来自2011年...如今它们的返回值使then()done()非常不同。由于then()通常仅使用成功回调函数进行调用,因此你的观点只是一个细节,而不是要记住/知道的主要内容。(无法说在jQuery 3.0之前是怎样的。) - Robert Siemer

16

deferred.done()

向Deferred对象添加处理程序,仅在其被解决时调用。可以添加多个回调函数。

var url = 'http://jsonplaceholder.typicode.com/posts/1';
$.ajax(url).done(doneCallback);

function doneCallback(result) {
    console.log('Result 1 ' + result);
}

您也可以像这样写在上面,

function ajaxCall() {
    var url = 'http://jsonplaceholder.typicode.com/posts/1';
    return $.ajax(url);
}

$.when(ajaxCall()).then(doneCallback, failCallback);

deferred.then()

添加回调函数,用于在延迟对象已解决、被拒绝或仍在进行中时被调用。

var url = 'http://jsonplaceholder.typicode.com/posts/1';
$.ajax(url).then(doneCallback, failCallback);

function doneCallback(result) {
    console.log('Result ' + result);
}

function failCallback(result) {
    console.log('Result ' + result);
}

你的帖子没有明确说明如果没有提供fail回调函数,then会如何行为 - 即根本不捕获fail情况。 - B M
失败的情况会引发异常,可以被程序的顶层捕获。你也可以在JavaScript控制台中看到这个异常。 - David Spector

11

实际上,jQuery的Deferred(延迟对象)与Promises有一个相当重要的区别(而且jQuery 3.0试图将它们带入规范之中)。

done/then之间的关键区别在于:

  • .done()无论你做什么或返回什么,它始终返回与它开始时相同的Promise/wrapped值。
  • .then()始终返回一个新的Promise,您可以通过控制传递给它的函数返回值来控制该Promise。

从jQuery翻译成本地ES2015 Promises后,.done()在某种程度上像是在Promise链中的函数周围实现了一个“tap”结构,因为如果链处于“resolve”状态,它将把一个值传递给函数…但该函数的结果不会影响链本身。

const doneWrap = fn => x => { fn(x); return x };

Promise.resolve(5)
       .then(doneWrap( x => x + 1))
       .then(doneWrap(console.log.bind(console)));

$.Deferred().resolve(5)
            .done(x => x + 1)
            .done(console.log.bind(console));

这两个都会记录5,而不是6。

请注意,我使用了done和doneWrap来进行日志记录,而不是.then。这是因为console.log函数实际上不会返回任何内容。如果您传递给.then一个不返回任何内容的函数会发生什么情况呢?

Promise.resolve(5)
       .then(doneWrap( x => x + 1))
       .then(console.log.bind(console))
       .then(console.log.bind(console));

那将记录:

5

undefined

发生了什么?当我使用.then并传递一个没有返回任何内容的函数时,它的隐含结果是"undefined"......这当然会返回一个 Promise[undefined] 给下一个 then 方法,然后记录 undefined。因此,我们最初启动的值基本上丢失了。

.then() 本质上是一种函数组合形式:每个步骤的结果被用作下一步中函数的参数。这就是为什么 .done 最好被认为是一个“tap”-> 它实际上不是组合的一部分,而只是在特定步骤查看值并运行函数的一种方式,但实际上并不改变组合的方式。

这是一个非常基本的区别,原生的 Promises 没有 .done 方法实现自己可能有很好的理由。我们甚至不必进入为什么没有 .fail 方法,因为那更加复杂(即,在 .catch 中返回裸值的函数不像那些传递给 .then 的函数那样“保持”拒绝状态,它们被解析!)


8

then()总是表示无论何种情况都会被调用。但在不同的jQuery版本中,传递的参数是不同的。

在jQuery 1.8之前,then()等同于done().fail()。而且所有的回调函数都共享相同的参数。

但是从jQuery 1.8开始,then()返回一个新的promise,如果它返回了一个值,那么这个值将被传递到下一个回调函数中。

让我们看下面的例子:

var defer = jQuery.Deferred();

defer.done(function(a, b){
            return a + b;
}).done(function( result ) {
            console.log("result = " + result);
}).then(function( a, b ) {
            return a + b;
}).done(function( result ) {
            console.log("result = " + result);
}).then(function( a, b ) {
            return a + b;
}).done(function( result ) {
            console.log("result = " + result);
});

defer.resolve( 3, 4 );

在 jQuery 1.8 之前,答案应该是:
result = 3
result = 3
result = 3

所有的result都需要3。而then()函数总是将相同的延迟对象传递给下一个函数。

但是在jQuery 1.8中,结果应该是:

result = 3
result = 7
result = NaN

由于第一个then()函数返回一个新的Promise,值7(这是唯一传递的参数)被传递给下一个done(),所以第二个done()写下result = 7。第二个then()以7作为a的值并将undefined作为b的值,因此第二个then()返回一个带有参数NaN的新Promise,最后一个done()打印出NaN作为其结果。


“then()总是意味着无论如何都会被调用” - 这并不正确。如果Promise内部出现错误,then()将永远不会被调用。 - David Spector
有趣的一点是,jQuery.Deferred() 可以接收多个值,并将其正确传递给第一个 .then()。但有点奇怪...因为任何后续的 .then() 都不能这样做。(通过 return 选择的接口只能返回一个值。)JavaScript 的原生 Promise 并不会这样做。(说实话更加一致。) - Robert Siemer

5

仅使用.then()

.done()没有任何优势,而且存在以下缺点:

  • 不能被正确连接
    • a.done().done()等同于a.done(); a.done(),可以用a.then(); a.then()代替
    • .done()无法实现a.then().then()
  • 阻塞了resolve()调用(所有.done()处理程序将同步执行)
  • resolve()可能会受到已注册的.done()处理程序异常的影响!
  • .done()中的异常会使deferred出现问题:
    • 更多.done()处理程序将被静默跳过
  • .then()没有这些问题

我曾暂时认为.then(oneArgOnly)总是需要.catch(),以便不会忽略任何异常,但现在不再是这样:默认情况下,unhandledrejection事件会在控制台上记录未处理的.then()异常。非常合理!完全没有理由继续使用.done()

证明

以下代码片段揭示了:

  • 所有.done()处理程序将在resolve()点同步调用
    • 作为1、3、5、7记录
    • 在脚本到达底部之前记录
  • .done()中的异常会影响resolve()调用者
    • 通过包围resolve()的catch进行记录
  • .done()中的异常会破坏promise继续被.done()解决
    • 8和10没有被记录!
  • .then()没有这些问题
    • 在线程空闲后记录为2、4、6、9、11
    • (片段环境似乎没有unhandledrejection

顺便说一下,.done()中的异常无法被正确捕获:由于.done()的同步模式,错误要么在.resolve()点抛出(可能是库代码!),要么在已解决deferred上的.done()调用时附加出错。

console.log('Start of script.');
let deferred = $.Deferred();
// deferred.resolve('Redemption.');
deferred.fail(() => console.log('fail()'));
deferred.catch(()=> console.log('catch()'));
deferred.done(() => console.log('1-done()'));
deferred.then(() => console.log('2-then()'));
deferred.done(() => console.log('3-done()'));
deferred.then(() =>{console.log('4-then()-throw');
    throw 'thrown from 4-then()';});
deferred.done(() => console.log('5-done()'));
deferred.then(() => console.log('6-then()'));
deferred.done(() =>{console.log('7-done()-throw');
    throw 'thrown from 7-done()';});
deferred.done(() => console.log('8-done()'));
deferred.then(() => console.log('9-then()'));

console.log('Resolving.');
try {
    deferred.resolve('Solution.');
} catch(e) {
    console.log(`Caught exception from handler
        in resolve():`, e);
}
deferred.done(() => console.log('10-done()'));
deferred.then(() => console.log('11-then()'));
console.log('End of script.');
<script
src="https://code.jquery.com/jquery-3.4.1.min.js"
integrity="sha384-vk5WoKIaW/vJyUAd9n/wmopsmNhiy+L2Z+SBxGYnUkunIxVxAv/UtMOhba/xskxh"
crossorigin="anonymous"
></script>


几点意见:1) 我明白你的意思,如果之前的 done 抛出异常,它将不会被执行。但为什么要默默地忽略呢?我的意思是,既然发生了异常,为什么你说它是无声的呢? 2) 我非常讨厌 Deferred 对象,因为它的API设计得非常非常糟糕。它太复杂和混乱了。你在这里的代码也没有证明你的观点,并且它过于复杂而超出了你所要证明的范围。 3) 为什么在第二个 then 之前执行索引为2、4和6的 done - CodingYoshi
我的错,你肯定值得一票。至于你关于异常的评论,通常情况下,这就是异常的工作方式:一旦引发,之后的代码将不会被执行。此外,jQuery文档指出,只有在延迟解决后才会执行。 - CodingYoshi
@CodingYoshi 这里的情况有所不同:我只是在谈论已解决的 promises/deferreds。我并不抱怨其余的 success-handler 没有被调用,这是正常的。但我看不出为什么一个完全不同的成功处理程序在成功的 promise 上不会被调用。所有的 .then() 都会被调用,无论是否引发异常(在这些处理程序中)。但是附加/剩余的 .done() 却会中断。 - Robert Siemer

4

从jQuery 3.0开始,还有一个非常重要的区别可能会导致意外行为,并且之前的答案中没有提到:

考虑以下代码:

let d = $.Deferred();
d.done(() => console.log('then'));
d.resolve();
console.log('now');
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.0.0/jquery.min.js"></script>

这将输出:
then
now

现在,在相同的片段中,将代码done()替换为then()

var d = $.Deferred();
d.then(() => console.log('then'));
d.resolve();
console.log('now');
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.0.0/jquery.min.js"></script>

输出结果为:

now
then

因此,对于立即解决的延迟对象(Deferred),传递给done()的函数总是以同步方式调用,而传递给then()的任何参数都是异步调用。
这与先前的jQuery版本不同,其中两个回调都会同步调用,正如在升级指南中所述:
引用:

Promises/A+兼容性所需的另一个行为更改是Deferred.then()回调始终以异步方式调用。 之前,如果将.then()回调添加到已经解析或拒绝的Deferred中,回调将立即且同步运行。


谢谢。这个答案解释了我看到的行为。我正在使用 then()。我的测试失败是因为回调是异步调用的,在测试结束后才被调用。使用 done(),回调被同步调用,满足测试期望,测试通过了。 - Shant Dashjian

3

1
除了上面的答案之外:
.then 的真正威力在于以流畅的方式链接 ajax 调用,从而避免回调地狱。
例如:
$.getJSON( 'dataservice/General', {action:'getSessionUser'} )
    .then( function( user ) {
        console.log( user );
        return $.getJSON( 'dataservice/Address', {action:'getFirstAddress'} );
    })
    .then( function( address ) {
        console.log( address );
    })

在这里,第二个.then跟随返回的$.getJSON。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接