Цикл while с промисами

Какой идиоматический способ сделать что-то вроде цикла while с промисами. Так:

сделать что-нибудь, если условие остается в силе сделать это снова повторить, а затем сделать что-то еще.

dosomething.then(possilblydomoresomethings).then(finish)

Я сделал это таким образом, мне было интересно, есть ли какие-нибудь лучшие/более идоматические способы?

var q = require('q');

var index = 1;

var useless =  function(){
        var currentIndex = index;
        console.log(currentIndex)
        var deferred = q.defer();
        setTimeout(function(){
            if(currentIndex > 10)
                deferred.resolve(false);
            else deferred.resolve(true);
            },500);
        return deferred.promise;
    }

var control = function(cont){
        var deferred = q.defer();
        if(cont){
                index = index + 1;
                useless().then(control).then(function(){
                        deferred.resolve();
                    });
            }
         else deferred.resolve();
        return deferred.promise;
    }

var chain = useless().then(control).then(function(){console.log('done')});

Вывод: 1 2 3 4 5 6 7 8 9 10 11 сделано


person Grummle    schedule 20.06.2013    source источник
comment
Мне было интересно, есть ли какие-нибудь лучшие/более идоматические способы? Нет, рекурсия - это путь.   -  person juandopazo    schedule 20.06.2013
comment
Как бы вы это сделали без рекурсии? У меня возникло ощущение, что рекурсия, вероятно, не крутая, но я не мог понять, как без этого обойтись. Любые идеи?   -  person Grummle    schedule 20.06.2013
comment
Я не думаю, что вы можете использовать итерацию без какого-либо механизма остановки, такого как Taskjs.   -  person juandopazo    schedule 21.06.2013
comment
Вы можете избежать рекурсии, если в вашей среде есть async/await; см. мой обновленный ответ ниже.   -  person lawrence    schedule 04.04.2018


Ответы (13)


Я бы использовал объект для переноса значения. Таким образом, вы можете иметь свойство done, чтобы цикл знал, что вы закончили.

// fn should return an object like
// {
//   done: false,
//   value: foo
// }
function loop(promise, fn) {
  return promise.then(fn).then(function (wrapper) {
    return !wrapper.done ? loop(Q(wrapper.value), fn) : wrapper.value;
  });
}

loop(Q.resolve(1), function (i) {
  console.log(i);
  return {
    done: i > 10,
    value: i++
  };
}).done(function () {
  console.log('done');
});
person juandopazo    schedule 21.06.2013
comment
Обратите внимание, что это, вероятно, будет потреблять всю доступную память после достаточно долгого времени работы; кажется, что-то сохраняется в каждом цикле обещания, по крайней мере, с Q. - person Asherah; 18.10.2013
comment
@juandopazo в вашем примере вы должны изменить i++ на ++i, иначе вы получите бесконечный цикл. - person PauloASilva; 26.11.2015
comment
Я успешно использовал это решение, даже используя библиотеку require ('promise');. Теперь мне интересно, можно ли построить нерекурсивное решение, см. stackoverflow.com/questions/36361827/ - person Galder Zamarreño; 01.04.2016
comment
Эш, не могли бы вы уточнить проблему с памятью? Это с оберткой? - person lgc_ustc; 03.02.2018

Вот многоразовая функция, которая, я думаю, довольно ясна.

var Q = require("q");

// `condition` is a function that returns a boolean
// `body` is a function that returns a promise
// returns a promise for the completion of the loop
function promiseWhile(condition, body) {
    var done = Q.defer();

    function loop() {
        // When the result of calling `condition` is no longer true, we are
        // done.
        if (!condition()) return done.resolve();
        // Use `when`, in case `body` does not return a promise.
        // When it completes loop again otherwise, if it fails, reject the
        // done promise
        Q.when(body(), loop, done.reject);
    }

    // Start running the loop in the next tick so that this function is
    // completely async. It would be unexpected if `body` was called
    // synchronously the first time.
    Q.nextTick(loop);

    // The promise
    return done.promise;
}


// Usage
var index = 1;
promiseWhile(function () { return index <= 11; }, function () {
    console.log(index);
    index++;
    return Q.delay(500); // arbitrary async
}).then(function () {
    console.log("done");
}).done();
person Stuart K    schedule 21.06.2013
comment
Это здорово! Я портировал ваш пример для RSVP.js: jsfiddle.net/wcW4r/1 Это может быть полезно для Пользователи Ember.js. - person miguelcobain; 21.05.2014
comment
Новая версия более идиоматична для RSVP и содержит обертки Promise в теле и состоянии: jsfiddle.net/wcW4r/3 - person miguelcobain; 21.05.2014
comment
Это может проглатывать исключения. Лучше использовать Q.fcall(body).then(loop,done.reject); - person Tzanko Matev; 19.09.2014
comment
Странно, это дает мне ошибку: Ошибка: ReferenceError: setTimeout не определен при сбросе (vendor/q.js:121:21) - person Rambatino; 14.11.2014
comment
Это зависит от рекурсии, так как же тогда она будет масштабироваться? Что происходит при попытке сделать 1000 звонков? - person vitaly-t; 05.04.2015
comment
Поскольку каждое промис выполняется в следующем такте цикла событий, стек очищается после каждой итерации, поэтому типичные проблемы с рекурсией (такие как переполнение стека) здесь не применяются. - person Stuart K; 17.05.2015
comment
поэтому я использую асинхронные методы, вы никогда не должны помещать их в синхронный цикл for или, если уж на то пошло, никогда не оборачивать асинхронный код синхронным кодом. - person PositiveGuy; 31.07.2015

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

const doSomething = value =>
  new Promise(resolve => 
    setTimeout(() => resolve(value >= 5 ? 'ok': 'no'), 1000))

const loop = value =>
  doSomething(value).then(result => {
    console.log(value)
    if (result === 'ok') {
      console.log('yay')      
    } else {
      return loop(value + 1)
    }
  })

loop(1).then(() => console.log('all done!'))

Посмотрите, как это работает на JSBin

Если бы вы использовали промис, который разрешает или отклоняет, вы бы определили then и catch вместо использования условного предложения.

Если бы у вас был массив промисов, вы бы просто меняли loop на сдвиг или выталкивали следующий каждый раз.


РЕДАКТИРОВАТЬ: Вот версия, в которой используется async/await, потому что это 2018 год:

const loop = async value => {
  let result = null
  while (result != 'ok') {
    console.log(value)
    result = await doSomething(value)
    value = value + 1
  }
  console.log('yay')
}

Посмотрите на CodePen

Как видите, здесь используется обычный цикл while, а не рекурсия.

person lawrence    schedule 15.07.2016
comment
Очень красивое решение! Я адаптировал это, чтобы передавать другую переменную через цепочку методов для каждого промиса. Спасибо за отличный простой пример! - person ozOli; 15.09.2017
comment
более полезны, чем другие ответы - person Omar; 24.11.2019

Это для bluebird, а не для q, но, поскольку вы не упомянули q конкретно... в API-документе bluebird автор упоминает, что возврат функции, генерирующей обещания, будет более идиоматичен, чем использование отложенных.

var Promise = require('bluebird');
var i = 0;

var counter = Promise.method(function(){
    return i++;
})

function getAll(max, results){
    var results = results || [];
    return counter().then(function(result){
        results.push(result);
        return (result < max) ? getAll(max, results) : results
    })
}

getAll(10).then(function(data){
    console.log(data);
})
person aarosil    schedule 17.06.2014
comment
Это было полезно, используйте рекурсивную функцию для реализации цикла while. Спасибо. - person Steve Kehlet; 13.12.2014
comment
Мне также было полезно увидеть это, но я обеспокоен (для моего использования - итерация всех ключей Redis с помощью SCAN), что рекурсия будет генерировать слишком много стека и либо не работать для больших наборов данных, либо потреблять ненужную память. Я думаю, что генераторы es6 могут быть тем путем, по которому мне нужно идти. - person wkw; 03.09.2016
comment
Это не совсем то, что я искал, но помогло мне найти решение. - person Gustavo Straube; 16.06.2017

Поскольку я не могу комментировать ответ Стюарта К., я немного добавлю сюда. Основываясь на ответе Стюарта К., вы можете свести его к удивительно простой концепции: Повторно использовать невыполненное обещание. Что у него есть по существу:

  1. Создать новый экземпляр отложенного обещания
  2. Определите свою функцию, которую вы хотите вызвать в цикле
  3. Inside that function:
    1. Check to see if you're done; and when you are resolve the promise created in #1 and return it.
    2. Если вы еще не закончили, скажите Q использовать существующее обещание и запустить невыполненную функцию, которая является «рекурсивной» функцией, или потерпеть неудачу, если она умерла. Q.when(promise, yourFunction, failFunction)
  4. После определения вашей функции используйте Q для запуска функции в первый раз, используя Q.nextTick(yourFunction)
  5. Наконец, верните свое новое обещание вызывающей стороне (что запустит все это).

Ответ Стюарта для более общего решения, но основы потрясающие (как только вы поймете, как это работает).

person millebi    schedule 07.05.2015

Этот шаблон теперь проще вызывать с помощью q-flow. Пример для вышеуказанной проблемы:

var q = require('q');
require('q-flow');
var index = 1;
q.until(function() {
  return q.delay(500).then(function() {
    console.log(index++);
    return index > 10;
  });
}).done(function() {
  return console.log('done');
});
person Joe Hildebrand    schedule 14.05.2014
comment
Это здорово, можно ли сделать это с bluebird, чтобы мне не нужно было использовать две разные библиотеки промисов вместе? - person Trevor; 02.05.2018

Вот расширение прототипа Promise для имитации поведения цикла for. Он поддерживает промисы или немедленные значения для разделов инициализации, условия, тела цикла и приращения. Он также имеет полную поддержку исключений и не имеет утечек памяти. Ниже приведен пример того, как его использовать.

var Promise = require('promise');


// Promise.loop([properties: object]): Promise()
//
//  Execute a loop based on promises. Object 'properties' is an optional
//  argument with the following fields:
//
//  initialization: function(): Promise() | any, optional
//
//      Function executed as part of the initialization of the loop. If
//      it returns a promise, the loop will not begin to execute until
//      it is resolved.
//
//      Any exception occurring in this function will finish the loop
//      with a rejected promise. Similarly, if this function returns a
//      promise, and this promise is reject, the loop finishes right
//      away with a rejected promise.
//
//  condition: function(): Promise(result: bool) | bool, optional
//
//      Condition evaluated in the beginning of each iteration of the
//      loop. The function should return a boolean value, or a promise
//      object that resolves with a boolean data value.
//
//      Any exception occurring during the evaluation of the condition
//      will finish the loop with a rejected promise. Similarly, it this
//      function returns a promise, and this promise is rejected, the
//      loop finishes right away with a rejected promise.
//
//      If no condition function is provided, an infinite loop is
//      executed.
//
//  body: function(): Promise() | any, optional
//
//      Function acting as the body of the loop. If it returns a
//      promise, the loop will not proceed until this promise is
//      resolved.
//
//      Any exception occurring in this function will finish the loop
//      with a rejected promise. Similarly, if this function returns a
//      promise, and this promise is reject, the loop finishes right
//      away with a rejected promise.
//
//  increment: function(): Promise() | any, optional
//
//      Function executed at the end of each iteration of the loop. If
//      it returns a promise, the condition of the loop will not be
//      evaluated again until this promise is resolved.
//
//      Any exception occurring in this function will finish the loop
//      with a rejected promise. Similarly, if this function returns a
//      promise, and this promise is reject, the loop finishes right
//      away with a rejected promise.
//
Promise.loop = function(properties)
{
    // Default values
    properties = properties || {};
    properties.initialization = properties.initialization || function() { };
    properties.condition = properties.condition || function() { return true; };
    properties.body = properties.body || function() { };
    properties.increment = properties.increment || function() { };

    // Start
    return new Promise(function(resolve, reject)
    {
        var runInitialization = function()
        {
            Promise.resolve().then(function()
            {
                return properties.initialization();
            })
            .then(function()
            {
                process.nextTick(runCondition);
            })
            .catch(function(error)
            {
                reject(error);
            });
        }

        var runCondition = function()
        {
            Promise.resolve().then(function()
            {
                return properties.condition();
            })
            .then(function(result)
            {
                if (result)
                    process.nextTick(runBody);
                else
                    resolve();
            })
            .catch(function(error)
            {
                reject(error);
            });
        }

        var runBody = function()
        {
            Promise.resolve().then(function()
            {
                return properties.body();
            })
            .then(function()
            {
                process.nextTick(runIncrement);
            })
            .catch(function(error)
            {
                reject(error);
            });
        }

        var runIncrement = function()
        {
            Promise.resolve().then(function()
            {
                return properties.increment();
            })
            .then(function()
            {
                process.nextTick(runCondition);
            })
            .catch(function(error)
            {
                reject(error);
            });
        }

        // Start running initialization
        process.nextTick(runInitialization);
    });
}


// Promise.delay(time: double): Promise()
//
//  Returns a promise that resolves after the given delay in seconds.
//
Promise.delay = function(time)
{
    return new Promise(function(resolve)
    {
        setTimeout(resolve, time * 1000);
    });
}


// Example
var i;
Promise.loop({
    initialization: function()
    {
        i = 2;
    },
    condition: function()
    {
        return i < 6;
    },
    body: function()
    {
        // Print "i"
        console.log(i);

        // Exception when 5 is reached
        if (i == 5)
            throw Error('Value of "i" reached 5');

        // Wait 1 second
        return Promise.delay(1);
    },
    increment: function()
    {
        i++;
    }
})
.then(function()
{
    console.log('LOOP FINISHED');
})
.catch(function(error)
{
    console.log('EXPECTED ERROR:', error.message);
});
person user3707531    schedule 25.12.2014

Я сейчас использую это:

function each(arr, work) {
  function loop(arr, i) {
    return new Promise(function(resolve, reject) {
      if (i >= arr.length) {resolve();}
      else try {
        Promise.resolve(work(arr[i], i)).then(function() { 
          resolve(loop(arr, i+1))
        }).catch(reject);
      } catch(e) {reject(e);}
    });
  }
  return loop(arr, 0);
}

Это принимает массив arr и функцию work и возвращает Promise. Предоставленная функция вызывается один раз для каждого элемента в массиве и получает текущий элемент и его индекс в массиве. Он может быть синхронным или асинхронным, и в этом случае он должен возвращать обещание.

Вы можете использовать его следующим образом:

var items = ['Hello', 'cool', 'world'];
each(items, function(item, idx) {
    // this could simply be sync, but can also be async
    // in which case it must return a Promise
    return new Promise(function(resolve){
        // use setTimeout to make this async
        setTimeout(function(){
            console.info(item, idx);
            resolve();
        }, 1000);
    });
})
.then(function(){
    console.info('DONE');
})
.catch(function(error){
    console.error('Failed', error);
})

Каждый элемент массива будет обрабатываться по очереди. Как только все будет обработано, запустится код, переданный .then(), или, если произошла какая-то ошибка, код, заданный .catch(). Внутри функции work вы можете throw и Error (в случае синхронных функций) или reject Promise (в случае асинхронных функций), чтобы прервать цикл.

function each(arr, work) {
  function loop(arr, i) {
    return new Promise(function(resolve, reject) {
      if (i >= arr.length) {resolve();}
      else try {
        Promise.resolve(work(arr[i], i)).then(function() { 
          resolve(loop(arr, i+1))
        }).catch(reject);
      } catch(e) {reject(e);}
    });
  }
  return loop(arr, 0);
}

var items = ['Hello', 'cool', 'world'];
each(items, function(item, idx) {
  // this could simply be sync, but can also be async
  // in which case it must return a Promise
  return new Promise(function(resolve){
    // use setTimeout to make this async
    setTimeout(function(){
      console.info(item, idx);
      resolve();
    }, 1000);
  });
})
.then(function(){
  console.info('DONE');
})
.catch(function(error){
  console.error('Failed', error);
})

person Stijn de Witt    schedule 18.06.2016

Вот общее решение, использующее промисы ES6:

/**
 * Simulates a while loop where the condition is determined by the result of a Promise.
 *
 * @param {Function} condition
 * @param {Function} action
 * @returns {Promise}
 */
function promiseWhile (condition, action) {
    return new Promise((resolve, reject) => {
        const loop = function () {
            if (!condition()) {
                resolve();
            } else {
                Promise.resolve(action())
                    .then(loop)
                    .catch(reject);
            }
        }
        loop();
    })
}

/**
 * Simulates a do-while loop where the condition is determined by the result of a Promise.
 *
 * @param {Function} condition
 * @param {Function} action
 * @returns {Promise}
 */
function promiseDoWhile (condition, action) {
    return Promise.resolve(action())
        .then(() => promiseWhile(condition, action));
}

export default promiseWhile;
export {promiseWhile, promiseDoWhile};

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

let myCounter = 0;

function myAsyncFunction () {
    return new Promise(resolve => {
        setTimeout(() => {
            console.log(++myCounter);
            resolve()
        }, 1000)
    });
}


promiseWhile(() => myCounter < 5, myAsyncFunction).then(() => console.log(`Timer completed: ${myCounter}`));
person Rory Abraham    schedule 03.03.2021

Используя обещание ES6, я придумал это. Он связывает обещания и возвращает обещание. Технически это не цикл while, но он показывает, как синхронно перебирать промисы.

function chain_promises(list, fun) {
    return list.reduce(
        function (promise, element) {
            return promise.then(function () {
                // I only needed to kick off some side-effects. If you need to get
                // a list back, you would append to it here. Or maybe use
                // Array.map instead of Array.reduce.
                fun(element);
            });
    	},
        // An initial promise just starts things off.
        Promise.resolve(true)
    );
}

// To test it...

function test_function (element) {
    return new Promise(function (pass, _fail) {
        console.log('Processing ' + element);
        pass(true);
    });
}

chain_promises([1, 2, 3, 4, 5], test_function).then(function () {
    console.log('Done.');
});

Вот моя скрипка.

person mqsoh    schedule 11.11.2015
comment
Совет: используйте Promise.resolve(true) вместо конструктора new Promise - person Bergi; 12.11.2015

Я подумал, что с тем же успехом мог бы бросить свою шляпу на ринг, используя обещания ES6...

function until_success(executor){
    var before_retry = undefined;
    var outer_executor = function(succeed, reject){
        var rejection_handler = function(err){
            if(before_retry){
                try {
                    var pre_retry_result = before_retry(err);
                    if(pre_retry_result)
                        return succeed(pre_retry_result);
                } catch (pre_retry_error){
                    return reject(pre_retry_error);
                }
            }
            return new Promise(executor).then(succeed, rejection_handler);                
        }
        return new Promise(executor).then(succeed, rejection_handler);
    }

    var outer_promise = new Promise(outer_executor);
    outer_promise.before_retry = function(func){
        before_retry = func;
        return outer_promise;
    }
    return outer_promise;
}

Аргумент executor такой же, как и переданный в конструктор Promise, но будет вызываться неоднократно, пока не вызовет обратный вызов успеха. Функция before_retry позволяет настраивать обработку ошибок при неудачных попытках. Если он возвращает истинное значение, это будет считаться формой успеха, и «цикл» завершится с этим правдивым результатом. Если функция before_retry не зарегистрирована или возвращает ложное значение, цикл будет выполняться для другой итерации. Третий вариант заключается в том, что функция before_retry сама выдает ошибку. Если это произойдет, то «цикл» завершится, передавая эту ошибку как ошибку.


Вот пример:

var counter = 0;
function task(succ, reject){
    setTimeout(function(){
        if(++counter < 5)
            reject(counter + " is too small!!");
        else
            succ(counter + " is just right");
    }, 500); // simulated async task
}

until_success(task)
        .before_retry(function(err){
            console.log("failed attempt: " + err);
            // Option 0: return falsey value and move on to next attempt
            // return

            // Option 1: uncomment to get early success..
            //if(err === "3 is too small!!") 
            //    return "3 is sort of ok"; 

            // Option 2: uncomment to get complete failure..
            //if(err === "3 is too small!!") 
            //    throw "3rd time, very unlucky"; 
  }).then(function(val){
       console.log("finally, success: " + val);
  }).catch(function(err){
       console.log("it didn't end well: " + err);
  })

Выход для варианта 0:

failed attempt: 1 is too small!!
failed attempt: 2 is too small!!
failed attempt: 3 is too small!!
failed attempt: 4 is too small!!
finally, success: 5 is just right

Выход для варианта 1:

failed attempt: 1 is too small!!
failed attempt: 2 is too small!!
failed attempt: 3 is too small!!
finally, success: 3 is sort of ok

Выход для варианта 2:

failed attempt: 1 is too small!!
failed attempt: 2 is too small!!
failed attempt: 3 is too small!!
it didn't end well: 3rd time, very unlucky
person dan-man    schedule 03.03.2016

Здесь много ответов, и то, чего вы пытаетесь достичь, не очень практично. но это должно работать. Это было реализовано в лямбда-функции aws, с Node.js 10 она будет работать до истечения времени ожидания функции. Он также может потреблять приличный объем памяти.

exports.handler = async (event) => {
  let res = null;
  while (true) {
    try{
     res = await dopromise();
    }catch(err){
     res = err;
    }
    console.log(res);
   }//infinite will time out
  };

  function dopromise(){
   return new Promise((resolve, reject) => {
    //do some logic
    //if error reject
        //reject('failed');
    resolve('success');
  });
}

Проверено на лямбде и работает нормально более 5 минут. Но, как утверждают другие, это нехорошо.

person Hans-Eric Lippke    schedule 31.05.2019

Я написал модуль, который помогает вам выполнять цепные циклы асинхронных задач с обещаниями, он основан на приведенном выше ответе, предоставленном juandopazo.

/**
 * Should loop over a task function which returns a "wrapper" object
 * until wrapper.done is true. A seed value wrapper.seed is propagated to the
 * next run of the loop.
 *
 * todo/maybe? Reject if wrapper is not an object with done and seed keys.
 *
 * @param {Promise|*} seed
 * @param {Function} taskFn
 *
 * @returns {Promise.<*>}
 */
function seedLoop(seed, taskFn) {
  const seedPromise = Promise.resolve(seed);

  return seedPromise
    .then(taskFn)
    .then((wrapper) => {
      if (wrapper.done) {
        return wrapper.seed;
      }

      return seedLoop(wrapper.seed, taskFn);
    });
}

// A super simple example of counting to ten, which doesn't even
// do anything asynchronous, but if it did, it should resolve to 
// a promise that returns the { done, seed } wrapper object for the
// next call of the countToTen task function.
function countToTen(count) {
  const done = count > 10;
  const seed = done ? count : count + 1;

  return {done, seed};
}

seedLoop(1, countToTen).then((result) => {
  console.log(result); // 11, the first value which was over 10.
});

https://github.com/CascadeEnergy/promise-seedloop

person nackjicholson    schedule 21.10.2015