Цепочка асинхронных промисов с 1 разрешением и 1 отклонением?

У меня есть функция, которая должна сделать что-то асинхронное за несколько шагов. На каждом шагу он может дать сбой. Он может выйти из строя до шага 1, поэтому вы можете узнать результат сразу или через 1,5 секунды. Когда происходит сбой, он должен выполнить обратный вызов. То же самое, когда это удается. (Я использую когда намеренно, потому что это не просто если: важно время.)

Я думал, что промисы идеальны, потому что они асинхронны и разрешаются только один раз, но у них все еще есть проблема: когда они терпят неудачу? Я могу явно видеть, когда это удается (после последнего шага), но когда это терпит неудачу? Внутри/перед любым шагом.

Вот что у меня есть сейчас, но это смешно:

function clickSpeed() {
    return new Promise(function(resolve, reject) {
        if ( test('one') ) {
            return setTimeout(function() {
                if ( test('two') ) {
                    return setTimeout(function() {
                        if ( test('three') ) {
                            return setTimeout(function() {
                                console.log('resolving...');
                                resolve();
                            }, 500);
                        }
                        console.log('rejecting...');
                        reject();
                    }, 500);
                }
                console.log('rejecting...');
                reject();
            }, 500);
        }
        console.log('rejecting...');
        reject();
    });
}

(test() случайным образом проходит или не проходит шаг.)

Скрипка здесь: http://jsfiddle.net/rudiedirkx/zhdrjjx1/

Я предполагаю, что решение состоит в том, чтобы связать обещания, которые разрешают или отклоняют каждый шаг..? Может быть. Это вещь? Как бы я это реализовал?

Может ли это работать для неизвестного количества шагов?


person Rudie    schedule 23.11.2015    source источник
comment
Он терпит неудачу, как только вы позволяете ему потерпеть неудачу. Не уверен, что понимаю, в чем вопрос.   -  person Bergi    schedule 23.11.2015
comment
Каков ожидаемый результат return в return setTimeout(function() { ?   -  person guest271314    schedule 23.11.2015
comment
@guest271314: я думаю, что return здесь только для того, чтобы предотвратить запуск console.log('rejecting...'); reject();   -  person Bergi    schedule 23.11.2015
comment
@Bergi Когда я позволю этому потерпеть неудачу? Шаги асинхронные, поэтому я не знаю, удастся ли следующий шаг. Должен ли я проверять каждый шаг явно, как в приведенном выше коде?   -  person Rudie    schedule 23.11.2015
comment
@ guest271314 Возврат просто для того, чтобы прервать поток, чтобы он не отклонялся.   -  person Rudie    schedule 23.11.2015
comment
Я думаю, ты хочешь что-то вроде этого : promise.then(function(res, reject) { res('step1') or reject('err')}).then(function(res) { res('step2')}).catch(function(err) { console.log(err) })   -  person Errorpro    schedule 23.11.2015
comment
@Rudie: Да, вы должны явно допускать сбой на каждом этапе, потому что если вы этого не сделаете, он не сработает. Конечно, вы должны использовать промисы совсем по-другому.   -  person Bergi    schedule 23.11.2015
comment
но когда это терпит неудачу? Внутри/перед любым шагом. , Возврат просто прерывает поток, чтобы он не отклонялся. test не отклоняет обещание ? Как будет вызываться reject на основе возвращаемого значения test, если setTimeout прерывает поток?   -  person guest271314    schedule 23.11.2015
comment
@Bergi По-другому как? Конечно почему?   -  person Rudie    schedule 23.11.2015
comment
@guest271314 test() возвращает только логическое значение. Это может быть не функция, а оператор или проверка другого объекта/свойства. Отклонение происходит после каждого IF.   -  person Rudie    schedule 23.11.2015
comment
@guest271314 test() возвращает логическое значение, но случайное значение предназначено только для тестирования. Он берет что угодно откуда угодно, что может отклонить обещание или продвинуть его к следующему шагу. Проверкаtest() не всегда является функцией и не знает об обещаниях.   -  person Rudie    schedule 23.11.2015


Ответы (3)


Вы можете буквально переписать свое решение для обещаний:

function sleep(ms) {
    return new Promise(function(resolve) {
        setTimeout(resolve, ms);
    });
}

function clickSpeed() {
    if ( test('one') ) {
        return sleep(500).then(function() {
            if ( test('two') ) {
                return sleep(500).then(function() {
                    if ( test('three') ) {
                        return sleep(500).then(function() {
                            console.log('resolving...');
                        });
                    }
                    console.log('rejecting...');
                    return Promise.reject();
                });
            }
            console.log('rejecting...');
            return Promise.reject();
        });
    }
    console.log('rejecting...');
    return Promise.reject();
}

Тем не менее, это все еще довольно некрасиво. Я бы предпочел обратить if/else в качестве первого шага:

function clickSpeed() {
    if (!test('one')) {
        console.log('rejecting...');
        return Promise.reject();
    }
    return sleep(500).then(function() {
        if (!test('two')) {
            console.log('rejecting...');
            return Promise.reject();
        }
        return sleep(500).then(function() {
            if (!test('three')) {
                console.log('rejecting...');
                return Promise.reject();
            }
            return sleep(500).then(function() {
                console.log('resolving...');
            });
        });
    });
}

Но затем мы также можем отменить эти обратные вызовы. Обычно это невозможно, когда вы выполняете ветвление с помощью if, но в этом случае единственным альтернативным результатом является отклонение, похожее на throwing, и не будет выполняться цепочка обратных вызовов then.

function clickSpeed() {
    return Promise.resolve() // only so that the callbacks look alike, and to use throw
    .then(function() {
        if (!test('one')) {
            console.log('rejecting...');
            throw;
        }
        return sleep(500);
    }).then(function() {
        if (!test('two')) {
            console.log('rejecting...');
            throw;
        }
        return sleep(500)
    }).then(function() {
        if (!test('three')) {
            console.log('rejecting...');
            throw;
        }
        return sleep(500);
    }).then(function() {
        console.log('resolving...');
    });
}

Теперь вы можете заставить эти test вызывать само исключение, чтобы вам больше не нужны были if, и вы можете переместить оператор console.log('rejecting...'); в .catch().

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

function clickSpeed() {
    return ['one', 'two', 'three'].reduce(function(p, cur) {
        return p.then(function() {
            if (!test(cur))
                throw new Error('rejecting...');
            else
                return sleep(500);
        });
    }, Promise.resolve());
}
person Bergi    schedule 23.11.2015
comment
Можете описать эффект throw в посте? - person guest271314; 23.11.2015
comment
Это то, с чем я вроде как играл, но не мог понять: jsfiddle.net/rudiedirkx/ zhdrjjx1/1 Это больше похоже на это! - person Rudie; 23.11.2015
comment
@guest271314: throw; ничем не отличается от return Promise.reject(); внутри обратного вызова then. - person Bergi; 23.11.2015
comment
@Rudie: Ах, я пропустил взгляд на твою скрипку. Он страдает от антишаблона Promise конструктора и не работает, потому что вызов reject() не разрывает цепочку, как исключение. - person Bergi; 23.11.2015
comment
@Bergi Что возвращается onRejected из throw? Относительно антишаблона конструктора Promise stackoverflow.com/q/23803743/1048572 предполагается, что new Promise(function(resolve, reject){}) никогда не следует использовать? - person guest271314; 23.11.2015
comment
@guest271314: Ничего. То есть undefined. Так же, как аргумент для Promise.reject(). - person Bergi; 23.11.2015
comment
@guest271314: Guest271314: Что касается антишаблона конструктора Promise, пожалуйста, прочитайте ответы на связанный вопрос. Они не говорят никогда. На самом деле я правильно использую его в этом самом ответе (sleep). - person Bergi; 23.11.2015
comment
@Bergi Да, и да, именно так. Я знал, что есть хороший способ Promises. Этот способ еще более связан с функцией, вызывающей clickSpeed(), что, как я и надеялся, возможно. Спасибо! - person Rudie; 23.11.2015
comment
@Bergi Читать страницу уже несколько раз. В чем разница между sleep как в посте и function sleep(ms) { Promise.resolve().then(function() { /*setTimeout here*/ })} ? В частности, ссылка на ответ @thefourtheyes на programmers.stackexchange.com/a/279003 может означать, что конструктор вообще не используется? , в зависимости от производительности? Имея здесь трудности с попыткой однозначно интерпретировать, когда, а когда нет. Задал бы вопрос , хотя , вероятно , был бы закрыт как дубликат . Здесь не совсем понятно, как оценить, когда использовать шаблон конструктора или Promise.resolve()? - person guest271314; 23.11.2015
comment
@guest271314: Используйте Promise.resolve, когда можете. Иногда вы не можете, вам понадобится конструктор Promise. sleep - это только один из примеров, ваша реализация просто не работает. Попробуйте, чтобы понять, почему. Нет, это не имеет ничего общего с производительностью (которая зависит от библиотеки, вопрос программистов, на который вы ссылаетесь, похоже, обсуждает Bluebird). - person Bergi; 23.11.2015

Поскольку ваше время монотонно, а ваша «работа» предопределена, я бы реорганизовал код, чтобы использовать setInterval() с контекстом, чтобы отслеживать требуемую «работу» или «шаг». На самом деле вам даже не обязательно использовать Promise при этом, хотя вы все равно можете, и если вы хотите добавить обработчики цепочек, это хорошая идея:

function clickSpeed(resolve, reject) {
  var interval = setInterval(function(work) {
    try {
      var current = work.shift();
      if(!test(current)) { // Do current step's "work"
        clearInterval(interval); // reject on failure and clear interval
        console.log('rejecting...', current);
        reject();
      }
      else if(!work.length) { // If this was the last step
        clearInterval(interval); // resolve (success!) and clear interval
        console.log('resolving...');
        resolve();
      }
    }
    catch(ex) { // reject on exceptions as well
      reject(ex);
      clearInterval(interval);
    }
  }, 500, ['one', 'two', 'three']); // "work" array
}

Функция может быть вызвана «напрямую» с обработчиками разрешения/отклонения или использована в качестве аргумента конструктора Promise.

См. полный пример в модифицированном JSFiddle.


Чтобы ответить на комментарий Берги слишком много шаблонного кода, код можно написать более лаконично, без ведения журнала:

function clickSpeed(resolve, reject) {
    function done(success, val) {
        clearInterval(interval);
        success ? resolve(val) : reject(val);
    }

    var interval = setInterval(function(work) {
        try {
            if(test(work.shift()) || done(false)) {
                work.length || done(true);
            }
        }
        catch(ex) { // reject on exceptions as well
            done(false, ex);
        }
    }, 500, ['one', 'two', 'three']); // "work" array
}
person Amit    schedule 26.11.2015
comment
Эй, это очень умно! Мне это нравится. - person Rudie; 26.11.2015
comment
Я бы порекомендовал избегать setInterval при использовании промисов, это просто не подходит. И вы теряете безопасность бросков, и вам приходится возвращаться к обратным вызовам. Кроме того, слишком много шаблонного… Однообразие можно легко использовать в коде промисов так же легко (см. Обновление к моему ответу). - person Bergi; 26.11.2015
comment
@Bergi - решение на самом деле не использует промисы, но позволяет обертывать промисом. Обработка исключений запекается внутри, а механизм обратного вызова разработан (как описано в OP). Шаблон минимален (как показано в обновленной версии) и сравним с вашим решением, которое я тоже считаю хорошим. - person Amit; 26.11.2015
comment
Ой, я, должно быть, перечитал это заявление try catch, я возьму это обратно. Тем не менее, я думаю, именно необходимость try делает ваше решение немного длиннее моего :-) - person Bergi; 26.11.2015

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

Вместо :

функция, которая должна что-то сделать асинхронно, за несколько шагов

скажем:

функция, которая должна что-то сделать за несколько асинхронных шагов

Таким образом, мы могли бы сначала написать функцию, которая выполняет цепочку промисов и возвращает результирующий промис:

function doAnAsyncSequence() {
    return Promise.resolve()
    .then(function() {
        doSomethingAsync('one');
    })
    .then(function() {
        doSomethingAsync('two');
    })
    .then(function() {
        doSomethingAsync('three');
    });
}

И ради демонстрации мы можем написать doSomethingAsync() так, чтобы он возвращал обещание, которое имеет шанс 50:50 быть разрешенным: отклоненным (что здесь более полезно, чем задержка):

function doSomethingAsync(x) {
    return new Promise(function(resolve, reject) {
        if(Math.random() > 0.5 ) {
            resolve(x);
        } else {
            reject(x); // importantly, this statement reports the input argument `x` as the reason for failure, which can be read and acted on where doSomethingAsync() is called.
        }
    });
}

Затем центральная часть вопроса:

когда это не удается?

можно перефразировать:

когда это не удалось?

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

В случае doAnAsyncSequence() мы можем сделать это следующим образом:

doAnAsyncSequence().then(function(result) {
    console.log(result); // if this line executes, it will always log "three", the result of the *last* step in the async sequence.
}, function(reason) {
    console.log('error: ' + reason);
});

Хотя в doAnAsyncSequence() или doSomethingAsync() нет оператора console.log() :

  • в случае успеха мы можем наблюдать общий результат (который в этом примере всегда будет «три»).
  • в случае ошибки мы точно знаем, какой из асинхронных шагов вызвал сбой процесса («один», «два» или «три»).

Попробуйте


Итак, это теория.

Чтобы ответить на конкретный вопрос (как я понимаю)...

для doSomethingAsync() напишите:

function test_(value, delay) {
    return new Promise(function(resolve, reject) {
        //call your own test() function immediately
        var result = test(value);
        // resolve/reject the returned promise after a delay
        setTimeout(function() {
            result ? resolve() : reject(value);
        }, delay);
    });
}

для doAnAsyncSequence() напишите:

function clickSpeed() {
    var delayAfterTest = 500;
    return Promise.resolve()
    .then(function() {
        test_('one', delayAfterTest);
    })
    .then(function() {
        test_('two', delayAfterTest);
    })
    .then(function() {
        test_('three', delayAfterTest);
    });
}

И вызовите следующим образом:

clickSpeed().then(function() {
    console.log('all tests passed');
}, function(reason) {
    console.log('test sequence failed at: ' + reason);
});
person Roamer-1888    schedule 26.11.2015
comment
Очень читабельно, но мне нужен тайм-аут, потому что другие вещи делают что-то асинхронно, и мне приходится ждать ~ 500 мс. - person Rudie; 26.11.2015
comment
Как я уже сказал, мой doSomethingAsync() для демонстрации. В вашем реальном коде вы вполне можете написать что-то совершенно другое. - person Roamer-1888; 26.11.2015