593

我正在编写代码,它看起来像:

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() */ |     });
}                                        | }

有人告诉我这分别称为“延迟反模式”或“Promise构造函数反模式”,这段代码有什么不好的,为什么这叫做反模式

4

3 回答 3

414

Esailija创造的延迟反模式(现在是显式构造反模式)是一种常见的反模式,人们对 Promise 不熟悉,我第一次使用 Promise 时就自己做了。上面代码的问题是没有利用承诺链的事实。

Promise 可以链接,.then你可以直接返回 Promise。您的代码getStuffDone可以重写为:

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

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

仅当您将 API 转换为 Promise且无法自动执行此操作时,或者当您编写更容易以这种方式表达的聚合函数时,才应使用延迟对象。

引用 Esailija 的话:

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

于 2014-05-22T10:07:00.857 回答
155

它出什么问题了?

但模式有效!

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

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

如果另一个 Promise 被拒绝,这将在不被注意的情况下发生,而不是被传播到新的 Promise(它将被处理) - 并且新的 Promise 永远处于未决状态,这可能会导致泄漏。

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

相比之下, using.then()确实会自动处理这两种情况,并在发生错误时拒绝新的承诺:

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

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

但我已经处理了一切!

真的吗?好的。但是,这将非常详细和丰富,特别是如果您使用支持其他功能(如取消或消息传递)的 Promise 库。或者也许它会在未来,或者你想把你的图书馆换成更好的?你不会想为此重写你的代码。

库的方法 ( then) 不仅原生支持所有功能,它们还可能进行了某些优化。使用它们可能会使您的代码更快,或者至少允许通过库的未来版本进行优化。

我该如何避免呢?

因此,每当您发现自己手动创建一个PromiseDeferred已经存在的 Promise 时,请先检查库 API。Deferred 反模式通常被那些将 Promise [仅] 视为观察者模式的人应用 - 但Promise不仅仅是回调:它们应该是可组合的。每个体面的库都有许多易于使用的函数,用于以各种可能的方式组合 Promise,处理所有你不想处理的低级内容。

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

于 2014-08-29T13:28:03.287 回答
18

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

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

使用async functions,然后是await每个 Promise!

而不是手动构建嵌套的 Promise 链,例如:

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;
}

这有很多好处:

  • 调用async函数总是返回一个 Promise,它使用返回的值进行解析,如果在异步函数中抛出错误,则拒绝
  • 如果一个awaited Promise 被拒绝,错误 get 会被抛出到异步函数中,所以你可以try { ... } catch(error) { ... }像同步错误一样。
  • 您可以await在循环和 if 分支内部,使大部分 Promise 链逻辑变得微不足道
  • 尽管异步函数的行为大多类似于 Promise 链,但它们更容易阅读(也更容易推理)

我该如何await回拨?

如果回调只回调一次,并且您正在调用的 API 还没有提供 Promise(大多数都提供了!),这是使用 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 function返回结果?

不会。如果您调用异步函数,则始终会返回 Promise。然后,您也可以await在异步函数中使用该 Promise。您不能在同步函数中等待结果(您必须调用.then并附加回调)。

从概念上讲,同步functions 总是在一个作业中运行到完成,而async functions 同步运行直到它们到达一个await,然后它们继续在另一个作业中。

于 2021-03-22T13:33:14.027 回答