什么是显式Promise构造反模式,我该如何避免它?

IT技术 javascript promise q bluebird es6-promise
2020-12-11 21:24:44

我正在编写执行以下操作的代码:

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

3个回答

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

Promises 可以链接,.then你可以直接返回Promises 你的代码getStuffDone可以改写为:

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

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

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

引用 Esailija:

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

@mhelvens 如果您手动将非回调 API 拆分为适合“将回调 API 转换为Promise”部分的Promise API。反模式是关于无缘无故地将一个Promise包装在另一个Promise中,您并没有包装一个Promise,所以它在这里不适用。
2021-02-11 21:24:44
@BenjaminGruenbaum:我有信心为此使用延迟,所以不需要新问题。我只是认为这是您在答案中遗漏的用例。我正在做的事情看起来更像是聚合的反面,不是吗?
2021-02-14 21:24:44
@BenjaminGruenbaum:啊,虽然我认为延迟本身被认为是一种反模式,但是蓝鸟不赞成使用它们,并且您提到“将 API 转换为Promise”(这也是一种不包装Promise的情况)。
2021-02-15 21:24:44
@mhelvens 我想多余的延迟反模式对于它的实际作用会更准确。Bluebird 将.defer()api弃用到较新的(并抛出安全的)promise 构造函数中,它没有(绝不)弃用构造 Promise 的概念:)
2021-02-26 21:24:44
谢谢@Roamer-1888 你的参考帮助我终于弄清楚我的问题是什么。看起来我在没有意识到的情况下创建了嵌套的(未返回的)Promise。
2021-02-28 21:24:44

它出什么问题了?

但模式有效!

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

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

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

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

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

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

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

但是我什么都处理了!

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

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

我该如何避免?

因此,每当您发现自己手动创建一个PromiseorDeferred并且涉及到已经存在的Promise时,请先检查库 API递延反模式经常被人谁看到诺言应用[唯一]作为一个观察者模式-但Promise都比回调:他们应该是组合的。每个像样的库都有许多易于使用的函数,用于以各种可能的方式组合 Promise,处理您不想处理的所有低级内容。

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

我认为这里的重要教训之一,到目前为止还没有明确说明,是 Promise 及其链接的“then”代表一个异步操作:初始操作在 Promise 构造函数中,最终端点在 '然后'功能。所以如果你有一个同步操作,然后是一个异步操作,把同步的东西放在 Promise 中。如果您有一个异步操作后跟一个同步,请将同步内容放在“then”中。在第一种情况下,返回原始 Promise。在第二种情况下,返回 Promise/then 链(这也是一个 Promise)。
2021-02-07 21:24:44
@guest271314:所有不返回Promise的异步内容。尽管通常情况下,您可以使用库的专用Promise助手获得更好的结果。并确保始终在最底层进行promisify,所以它不是“一个函数包含setTimeout”,而是“函数setTimeout本身”。
2021-02-13 21:24:44
也许没有意识到明显的区别,在这里。 function () {setTimeout(dostuff, duration)}, setTimeout(dostuff, duration)? 能否描述两者之间的上下文差异,如果以上两种变体是或不是,“对 setTimeout 的调用与函数 setTimeout 明显不同”的准确表示
2021-02-17 21:24:44
@guest271314 只包含调用的函数与函数本身setTimeout明显不同,不是吗?setTimeout
2021-03-03 21:24:44
除了包含的函数之外,是否有示例setTimeout可以使用构造函数但不被视为“Promise 构造函数 anitpattern”?
2021-03-07 21:24:44

现在 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,它使用返回的值进行解析,如果在 async 函数中抛出错误则拒绝
  • 如果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-02-10 21:24:44
@john“停止执行异步函数”和“停止执行”之间存在巨大差异。
2021-02-20 21:24:44
更好的是:使用 async/await,并添加 eslint 规则no-floating-promisesno-misused-promises. 如果你忘记await了 Promise,linter 会冲你大喊大叫。
2021-03-08 21:24:44