Как правильно вырваться из цепочки обещаний?

Основываясь на вопросе здесь: jQuery цепочки и каскадирование тогда и когда и принятый ответ, я хочу разорвать цепочку обещаний в какой-то момент, но еще не нашел правильный путь. Есть несколько сообщений об этом, но я все еще потерян.

Взяв пример кода из исходного вопроса:

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

Ответ 1

Во-первых, я думаю, что лучше сказать, что вы пытаетесь "обходить" (часть) цепочку обещаний, а не "ломать" ее.

Как вы говорите, тестирование "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'
};

С положительной стороны, я считаю, что отсутствие гнездования позволяет лучше понять естественный путь успеха. Кроме того, для меня, по крайней мере, этот шаблон потребует минимального умственного манипулирования, чтобы при необходимости использовать дополнительные обходы.

В нижней части эта модель немного менее эффективна, чем у Берги. В то время как основной путь имеет такое же количество promises как Bergi's, путь cantinas.length == 0 требует еще один (или один на обход, если были закодированы несколько байпасов). Кроме того, этот шаблон требует надежного повторного обнаружения определенных условий (состояний) ошибки - следовательно, объект errMessages, который некоторые могут найти, умаляет.

Ответ 2

Звучит так, как будто ты хочешь разветвляться, а не ломаться - ты хочешь продолжать как обычно до done. Хорошим свойством обещаний является то, что они не только цепочками, но также могут быть вложенными и не вложенными без ограничений. В вашем случае вы можете просто поместить часть цепочки, которую вы хотите "разорвать" внутри вашего 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
});

Ответ 3

Для пользователей, использующих встроенный браузер promises, и ищет способ остановить цепочку обещаний, не информируя всех потребителей о случае отклонения, запуская любые прикованные then или catch es или бросая любые 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), но если это станет проблемой, возможно, вы можете создать реальный экземпляр обещания и средний его методы и методы catch.