如何正确地打破承诺链?

Posted

技术标签:

【中文标题】如何正确地打破承诺链?【英文标题】:How to properly break out of a promise chain? 【发布时间】:2015-06-12 12:31:16 【问题描述】:

基于这里的问题:jQuery chaining and cascading then's and when's 和接受的答案,我想在某个点打破承诺链,但还没有找到正确的方法。有multiplepostsabout这个,还是迷路了。

从原始问题中获取示例代码:

Menus.getCantinas().then(function(cantinas) // `then` is how we chain promises
    Menus.cantinas = cantinas;
    // if we need to aggregate more than one promise, we `$.when`
    return $.when(Menus.getMeals(cantinas), Menus.getSides(cantinas));
).then(function(meals, sides) // in jQuery `then` can take multiple arguments
    Menus.sides = sides; // we can fill closure arguments here
    Menus.meals = meals;
    return Menus.getAdditives(meals, sides); // again we chain
).then(function(additives)
    Menus.additives = additives;
    return Menus; // we can also return non promises and chain on them if we want
).done(function() // done terminates a chain generally.
     // edit html here
);

如果cantinas.length == 0,我将如何打破链条?我不想得到饭菜,也不想得到添加剂,坦率地说,我想称之为某种“空结果”回调。我尝试了以下非常丑陋(但有效......)。教我正确的方法。这仍然是一个有效的结果,因此本身不是“失败”,我想说的只是空结果。

var emptyResult = false;
Menus.getCantinas().then(function(cantinas)
    Menus.cantinas = cantinas;
    if (cantinas.length == 0) 
      emptyResult = true;
      return "emptyResult"; //unuglify me
    
    return $.when(Menus.getMeals(cantinas), Menus.getSides(cantinas));
).then(function(meals, sides) 
    if (meals == "emptyResult") return meals;  //look at my ugliness...
    Menus.sides = sides;
    Menus.meals = meals;
    return Menus.getAdditives(meals, sides);
).then(function(additives)
    if (additives == "emptyResult") return additives;
    Menus.additives = additives;
    return Menus;
).done(function()
   if (emptyResult)
     //do empty result stuff
   else
     // normal stuff
);

【问题讨论】:

另见Proper way to skip a then function in Q Promises 【参考方案1】:

听起来像你want to branch,not to break - 你想像往常一样继续done。 Promise 的一个很好的特性是它们不仅可以链接,还可以是 nested and unnested 没有限制。在你的情况下,你可以把你想要“打破”的链部分放在你的if-statement 中:

Menus.getCantinas().then(function(cantinas) 
    Menus.cantinas = cantinas;

    if (cantinas.length == 0)
        return Menus; // break!

    // else
    return $.when(Menus.getMeals(cantinas), Menus.getSides(cantinas))
    .then(function(meals, sides) 
        Menus.sides = sides;
        Menus.meals = meals;
        return Menus.getAdditives(meals, sides);
    ).then(function(additives) 
        Menus.additives = additives;
        return Menus;
    );
).done(function(Menus) 
    // with no cantinas, or with everything
);

【讨论】:

听起来是正确的方法,但是当返回Menus 时,所有以下调用仍在进行(即.then(function(meals, sides)...then(function(additives))。不应进行这些调用,因为没有cantinas(第一次调用为空)。返回Menus时,meals == Menus在下面then @DennisG:您需要移动带有mealsthen。它不应该再跟随getCantinas().then(…),但它应该嵌套在回调中。我希望缩进能说明这一点 这看起来正是 Promise 所要避免的,回调地狱 @Yerken:我在这里看不到任何回调地狱。所有这些函数return 都有用。 这是正确的解决方案;它与同步版本完美匹配,不会滥用承诺拒绝。 (你不会通过 throw 将任意错误跳出内部循环,只在外部 catch 它,对吗?)外部then 返回的内部承诺可以重构为它自己的函数,就像任何其他 javascript 重构。【参考方案2】:

首先,我认为最好说您正在寻求“绕过”(部分)承诺链,而不是“破坏”它。

正如您所说,在几个地方测试“emptyResult”非常难看。幸运的是,在遵循不执行某些承诺链的相同一般原则的同时,可以使用更优雅的机制。

另一种机制是使用承诺拒绝来控制流程,然后在链中稍后重新检测特定的错误条件,并将其放回成功路径。

Menus.getCantinas().then(function(cantinas) 
    Menus.cantinas = cantinas;
    if(cantinas.length == 0) 
        return $.Deferred().reject(errMessages.noCantinas);
     else 
        return $.when(Menus.getMeals(cantinas), Menus.getSides(cantinas));
    
).then(function(meals, sides) 
    Menus.sides = sides;
    Menus.meals = meals;
    return Menus.getAdditives(meals, sides);
).then(function(additives) 
    Menus.additives = additives;
    return Menus;
).then(null, function(err) 
    //This "catch" exists solely to detect the noCantinas condition 
    //and put the chain back on the success path.
    //Any genuine error will be propagated as such.
    //Note: you will probably want a bit of safety here as err may not be passed and may not be a string.
    return (err == errMessages.noCantinas) ? $.when(Menus) : err;
).done(function(Menus) 
    // with no cantinas, or with everything
);

var errMessages = 
    'noCantinas': 'no cantinas'
;

从好的方面来说,我发现没有嵌套可以提高自然成功路径的可读性。另外,至少对我来说,这种模式需要最少的心理调整来适应进一步的绕过,如果需要的话。

不利的一面是,这种模式的效率略低于 Bergi 的模式。虽然主路径与 Bergi 的承诺数量相同,但 cantinas.length == 0 路径需要一个以上的承诺(如果编码了多个绕过,则每个绕过一个)。此外,这种模式需要对特定错误条件进行可靠的重新检测 - 因此是 errMessages 对象 - 有些人可能会发现这会减损。

【讨论】:

我实际上更喜欢这种模式,尽管效率很高。我实际上相信错误对象是有道理的,因为正如您所说,可能还有其他情况。而这个return $.Deferred().reject(errMessages.noCantinas);在我眼里很美,虽然不是“真正的”拒绝,但没关系。 我真的应该对return $.Deferred().reject(errMessages.noCantinas); 发表评论——比如//bypass-rejection。令人放心的是,您可以在没有该评论的情况下理解该模式。【参考方案3】:

对于那些使用内置浏览器承诺并寻找一种方法来停止承诺链而不让所有消费者都知道拒绝案例、触发任何链接的 thencatches 或抛出任何 Uncaught (in promise) 的人错误,您可以使用以下内容:

var noopPromise = 
  then: () => noopPromise, 
  catch: () => noopPromise


function haltPromiseChain(promise) 
  promise.catch(noop)

  return noopPromise


// Use it thus:
var p = Promise.reject("some error")
p = haltPromiseChain(p)
p.catch(e => console.log(e)) // this never happens

基本上,noopPromise 是一个基本的存根承诺接口,它接受链接函数,但从不执行任何函数。这依赖于这样一个事实,即浏览器显然使用鸭子类型来确定某事是否是一个承诺,所以 YMMV(我在 Chrome 57.0.2987.98 中对此进行了测试),但如果这成为一个问题,你可能会创建一个实际的承诺实例和中性它的 then 和 catch 方法。

【讨论】:

很好的答案,谢谢。它也适用于节点——我们的团队依赖于 V8 中的这种鸭式打字行为,并结合 await 用于 Waterline 项目。为了模拟早期回报,我们已经开始尝试一些更复杂的选项,但这是一个很好的引导!今晚我会调查一下。如果我发现任何问题(例如内存泄漏),我会尝试并记得报告回来,但如果您或其他人很好奇并且我没有跟进,请随时在此处联系我以提醒。 这是一个很好的解决方案。比伪造错误或嵌套承诺链要好得多。谢谢!

以上是关于如何正确地打破承诺链?的主要内容,如果未能解决你的问题,请参考以下文章

自己的 Promisekit 承诺没有正确响应

打破承诺链 - 停止执行下一个“then”

打破承诺链并根据链中被破坏(拒绝)的步骤调用函数

如何使用链调用 Promise?

拒绝后承诺链继续

TypeScript:如何正确键入一个接受承诺并按原样返回该承诺的函数