我写的代码是这样的:

function getStuffDone(param) {           | function getStuffDone(param) {
    var d = Q.defer(); /* or $q.defer */ |     return new Promise(function(resolve, reject) {
    // or = new $.Deferred() etc.        |     // using a promise constructor
    myPromiseFn(param+1)                 |         myPromiseFn(param+1)
    .then(function(val) { /* or .done */ |         .then(function(val) {
        d.resolve(val);                  |             resolve(val);
    }).catch(function(err) { /* .fail */ |         }).catch(function(err) {
        d.reject(err);                   |             reject(err);
    });                                  |         });
    return d.promise; /* or promise() */ |     });
}                                        | }

有人告诉我这分别被称为“延迟反模式”或“承诺构造函数反模式”,这段代码有什么不好的地方,为什么它被称为反模式?


当前回答

有什么问题吗?

但这种模式是有效的!

幸运的你。不幸的是,它可能不会,因为您可能忘记了一些边缘情况。在我所见过的超过一半的事件中,作者忘记了处理错误处理程序:

return new Promise(function(resolve) {
    getOtherPromise().then(function(result) {
        resolve(result.property.example);
    });
})

如果另一个承诺被拒绝,这将被忽略,而不是传播到新承诺(在那里它将被处理)-并且新承诺永远处于等待状态,这可能会导致泄漏。

同样的事情也会发生在回调代码导致错误的情况下——例如,当result没有属性并且抛出异常时。这将得不到处理,新的承诺也将得不到解决。

相反,使用.then()会自动处理这两种情况,并在发生错误时拒绝新的承诺:

 return getOtherPromise().then(function(result) {
     return result.property.example;
 })

延迟的反模式不仅麻烦,而且容易出错。使用.then()进行链接要安全得多。

但我什么都搞定了!

真的吗?很好。然而,这将是非常详细和丰富的,特别是如果您使用的承诺库支持其他功能,如取消或消息传递。或者将来可能会这样,或者您希望将您的库与更好的库交换?您不需要为此重写代码。

库的方法(那时)不仅本地支持所有的特性,它们还可能进行了某些优化。使用它们可能会使你的代码更快,或者至少允许在将来的库修订中进行优化。

我该如何避免呢?

因此,当您发现自己手动创建Promise或Deferred并且涉及到已经存在的Promise时,请先检查库API。Deferred反模式通常被那些只将promise视为观察者模式的人所应用——但是promise不仅仅是回调:它们应该是可组合的。每个像样的库都有许多易于使用的函数,可以以各种可以想象的方式组合promise,处理所有您不想处理的低级内容。

如果您发现需要以现有helper函数不支持的新方式组合某些promise,那么使用不可避免的Deferreds编写自己的函数应该是您的最后选择。考虑切换到一个更有特色的库,和/或针对当前库提交一个bug。它的维护者应该能够从现有函数中派生组合,为您实现一个新的helper函数和/或帮助识别需要处理的边缘情况。

其他回答

7年后的今天,这个问题有了一个更简单的答案:

如何避免显式构造函数反模式?

使用异步函数,然后等待每一个承诺!

而不是手动构造嵌套的承诺链,比如这个:

function promised() {
  return new Promise(function(resolve) {
    getOtherPromise().then(function(result) {
      getAnotherPromise(result).then(function(result2) {
        resolve(result2);
      });
    });
  });
}

只要把你的函数变成async,使用await关键字停止函数的执行,直到Promise解决:

async function promised() {
   const result =  await getOtherPromise();
   const result2 = await getAnotherPromise(result);
   return result2;
}

这有很多好处:

Calling the async function always returns a Promise, which resolves with the returned value and rejects if an error get's thrown inside the async function If an awaited Promise rejects, the error get's thrown inside the async function, so you can just try { ... } catch(error) { ... } it like the synchronous errors. You can await inside loops and if branches, making most of the Promise chain logic trivial Although async functions behave mostly like chains of Promises, they are way easier to read (and easier to reason about)

如何等待回调?

如果回调函数只回调一次,并且你所调用的API还没有提供Promise(大多数API都提供了!),这是使用Promise构造函数的唯一原因:

 // Create a wrapper around the "old" function taking a callback, passing the 'resolve' function as callback
 const delay = time => new Promise((resolve, reject) =>
   setTimeout(resolve, time)
 ); 

 await delay(1000);

如果await停止执行,调用async函数是否直接返回结果?

不。如果调用async函数,则总是返回Promise。然后你也可以在异步函数中等待Promise。您不能在同步函数中等待结果(您必须调用.then并附加回调)。

从概念上讲,同步函数总是在一个作业中运行到完成,而异步函数则同步运行到等待,然后继续在另一个作业中运行。

有什么问题吗?

但这种模式是有效的!

幸运的你。不幸的是,它可能不会,因为您可能忘记了一些边缘情况。在我所见过的超过一半的事件中,作者忘记了处理错误处理程序:

return new Promise(function(resolve) {
    getOtherPromise().then(function(result) {
        resolve(result.property.example);
    });
})

如果另一个承诺被拒绝,这将被忽略,而不是传播到新承诺(在那里它将被处理)-并且新承诺永远处于等待状态,这可能会导致泄漏。

同样的事情也会发生在回调代码导致错误的情况下——例如,当result没有属性并且抛出异常时。这将得不到处理,新的承诺也将得不到解决。

相反,使用.then()会自动处理这两种情况,并在发生错误时拒绝新的承诺:

 return getOtherPromise().then(function(result) {
     return result.property.example;
 })

延迟的反模式不仅麻烦,而且容易出错。使用.then()进行链接要安全得多。

但我什么都搞定了!

真的吗?很好。然而,这将是非常详细和丰富的,特别是如果您使用的承诺库支持其他功能,如取消或消息传递。或者将来可能会这样,或者您希望将您的库与更好的库交换?您不需要为此重写代码。

库的方法(那时)不仅本地支持所有的特性,它们还可能进行了某些优化。使用它们可能会使你的代码更快,或者至少允许在将来的库修订中进行优化。

我该如何避免呢?

因此,当您发现自己手动创建Promise或Deferred并且涉及到已经存在的Promise时,请先检查库API。Deferred反模式通常被那些只将promise视为观察者模式的人所应用——但是promise不仅仅是回调:它们应该是可组合的。每个像样的库都有许多易于使用的函数,可以以各种可以想象的方式组合promise,处理所有您不想处理的低级内容。

如果您发现需要以现有helper函数不支持的新方式组合某些promise,那么使用不可避免的Deferreds编写自己的函数应该是您的最后选择。考虑切换到一个更有特色的库,和/或针对当前库提交一个bug。它的维护者应该能够从现有函数中派生组合,为您实现一个新的helper函数和/或帮助识别需要处理的边缘情况。

Esailija创造的延迟反模式(现在是显式构造反模式)是一种常见的反模式,它是初次使用承诺时我自己创建的。上面的代码的问题是,它没有利用承诺链的事实。

承诺可以与。then连接,你可以直接返回承诺。你在getStuffDone中的代码可以重写为:

function getStuffDone(param){
    return myPromiseFn(param+1); // much nicer, right?
}

承诺都是为了使异步代码更具可读性,并且在不隐藏这一事实的情况下表现得像同步代码。承诺表示对一次性操作值的抽象,它们抽象了编程语言中语句或表达式的概念。

只有在将API转换为承诺且不能自动完成时,或者在编写更容易以这种方式表示的聚合函数时,才应该使用延迟对象。

引用Esailija的话:

这是最常见的反模式。当您不真正理解承诺并将其视为美化的事件发射器或回调实用程序时,很容易陷入这种情况。让我们回顾一下:承诺是关于使异步代码保留同步代码中丢失的大部分属性,如扁平缩进和一个异常通道。