什么是显式承诺构造反模式,如何避免它?

What is the explicit promise construction antipattern and how do I avoid it?

提问人:Benjamin Gruenbaum 提问时间:5/22/2014 最后编辑:Benjamin Gruenbaum 更新时间:10/7/2023 访问量:136200

问:

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

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 构造函数反模式”,这段代码有什么不好,为什么这被称为反模式

JavaScript q 蓝鸟 ES6-承诺

评论

1赞 The Dembinski 1/7/2017
或者包装器中的块是反模式?catchgetStuffDone
3赞 Noah Freitas 8/31/2017
至少对于本机示例,您还具有不必要的函数包装器,用于 and 处理程序(即它可能只是 。一场完美的反模式风暴。Promise.then.catch.then(resolve).catch(reject)
11赞 Benjamin Gruenbaum 8/31/2017
@NoahFreitas,代码是出于教学目的而编写的。我写这个问题和答案是为了帮助那些在阅读大量代码后遇到这个问题的人:)
8赞 Alex 8/5/2021
奇怪的并排代码示例是怎么回事?以前从未在这里见过,觉得很混乱。必须检查修订历史记录才能了解两者都是同一事物的示例。
1赞 Benjamin Gruenbaum 1/31/2022
@Rainb更像是这个问题,并不适合那些可能还不知道单子是什么或它们链接的人。我敢肯定,那个小组在某个时候包括你(以及我):)

答:

452赞 Benjamin Gruenbaum 5/22/2014 #1

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

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

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

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

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

引用 Esailija 的话:

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

评论

0赞 mhelvens 9/26/2014
@BenjaminGruenbaum:我对使用延期很有信心,所以不需要一个新问题。我只是认为这是您的答案中缺少的用例。我所做的似乎更像是聚合的对立面,不是吗?
2赞 Benjamin Gruenbaum 9/26/2014
@mhelvens 如果您手动将非回调 API 拆分为符合“将回调 API 转换为 promise”部分的 promise API。反模式是关于无缘无故地将一个承诺包装在另一个承诺中,你一开始就没有包装一个承诺,所以它在这里不适用。
0赞 mhelvens 9/26/2014
@BenjaminGruenbaum:啊,虽然延迟本身被认为是一种反模式,但 bluebird 弃用了它们,你提到了“将 API 转换为承诺”(这也是一开始就不包装承诺的情况)。
0赞 Benjamin Gruenbaum 9/26/2014
@mhelvens 我想超额延迟反模式对于它的实际作用会更准确。Bluebird 将 api 弃用到较新的(并且安全的)promise 构造函数中,它没有(绝不)弃用构造 promise 的概念:).defer()
1赞 ghuroo 5/30/2017
谢谢你@Roamer-1888,你的参考资料帮助我终于弄清楚了我的问题是什么。看起来我在不知不觉中创建嵌套(未返回)的承诺。
169赞 Bergi 8/29/2014 #2

这是怎么回事?

但这种模式有效!

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

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

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

如果你的回调代码导致错误,也会发生同样的事情 - 例如,当没有 a 时,会抛出异常。这将得不到处理,使新的承诺得不到解决。resultproperty

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

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

延迟反模式不仅繁琐,而且容易出错。用于链接要安全得多。.then()

但我已经处理了一切!

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

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

我该如何避免它?

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

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

评论

1赞 guest271314 11/24/2015
除了函数之外,是否有示例,其中可以使用构造函数但不被视为“Promise 构造函数 anitpattern”?setTimeout
3赞 Bergi 11/24/2015
@guest271314:所有不返回 promise 的异步内容。尽管很多时候,您都可以通过图书馆的专用承诺助手获得更好的结果。并确保始终在最低级别进行承诺,因此它不是“包含 setTimeout 的函数”,而是“函数 setTimeout 本身”。
0赞 Bergi 11/24/2015
@guest271314 仅包含调用的函数与函数 setTimeout 本身明显不同,不是吗?setTimeout
7赞 David Spector 8/27/2019
我认为这里的一个重要教训,到目前为止还没有明确说明,是 Promise 及其链接的“then”代表一个异步操作:初始操作在 Promise 构造函数中,最终端点在“then”函数中。因此,如果你有一个同步操作,然后是一个异步操作,请将同步内容放在 Promise 中。如果您有一个异步操作,然后是同步,请将同步内容放在“然后”中。在第一种情况下,返回原始 Promise。在第二种情况下,返回 Promise/then 链(也是一个 Promise)。
1赞 Bergi 10/7/2023
@DanielKaplan 感谢您的反馈,我添加了周围的函数声明。当然,运行它们仍然没有意义,它们不是可执行的片段
30赞 Jonas Wilms 3/22/2021 #3

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

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

使用 s,然后每个 Promise!async functionawait

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

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

只需转动函数并使用关键字停止函数的执行,直到 Promise 解决:asyncawait

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

这有很多好处:

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

如何等待回电?

如果回调只回调一次,并且您调用的 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 停止执行,调用异步函数会直接返回结果吗?

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

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

评论

1赞 john k 6/21/2021
使用异步函数的全部意义在于,这样您就不会停止执行。
7赞 Jonas Wilms 6/22/2021
@john,“停止执行异步函数”和“停止执行”之间存在巨大差异。
3赞 Coderer 10/22/2021
更好的是:使用 async/await,并添加 eslint 规则和 .如果你忘记了承诺,小偷会对你大喊大叫。no-floating-promisesno-misused-promisesawait
0赞 trincot 5/27/2022
“停止执行......”令人困惑......事实是,执行从函数调用返回
0赞 Jonas Wilms 5/27/2022
@trincot,这只适用于第一个等待者。我愿意接受建议,但为了更好地表达这一点。规范说“恢复位于执行上下文堆栈顶部的执行上下文”,这可能是先前等待的承诺的结算(所以我想这更令人困惑)。