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

我写的代码是这样的:

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

120319 次浏览

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

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

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

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

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

引用Esailija:

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

有什么问题吗?

但这种模式是有效的!

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

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

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

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

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

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

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

但我什么都搞定了!

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

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

我该如何避免呢?

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

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

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

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

使用async functions,然后await每个承诺!

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

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

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

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

这有很多好处:

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

我怎么能await回调?

如果回调函数只回调一次,并且你所调用的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 function是否直接返回结果?

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

从概念上讲,同步的# eyz0总是在一个作业中运行到完成,而# eyz1则同步运行到await,然后继续在另一个作业中运行。