【问题标题】:While loop with promises带有承诺的while循环
【发布时间】:2013-06-17 13:14:21
【问题描述】:

用 Promise 做类似 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 完成

【问题讨论】:

  • “我想知道是否有更好/更惯用的方法?”不,递归是要走的路。
  • 没有递归你会怎么做?我觉得递归可能并不酷,但我无法弄清楚如何做到这一点。有什么见解吗?
  • 如果没有像 Taskjs 这样的“停在这里”机制,我认为你不能使用迭代。
  • 如果您的环境中有 async/await 可用,则可以避免递归;请参阅下面的更新答案。

标签: node.js loops promise q


【解决方案1】:

这是一个我认为很清楚的可重用函数。

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

【讨论】:

  • 这太棒了!我为 RSVP.js 移植了您的示例:jsfiddle.net/wcW4r/1 它可能对 Ember.js 用户有用。
  • 新版本更符合 RSVP 习惯,并且在正文和条件中带有 Promise “包装器”:jsfiddle.net/wcW4r/3
  • 这可以吞下异常。最好使用 Q.fcall(body).then(loop,done.reject);
  • 奇怪,这给了我一个错误:Error: ReferenceError: setTimeout is not defined at flush (vendor/q.js:121:21)
  • 这依赖于递归,那么它将如何扩展呢?尝试拨打 1000 次电话时会发生什么情况?
【解决方案2】:

我会使用一个对象来包装这个值。这样你就可以拥有一个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');
});

【讨论】:

  • 请注意,这可能会在运行足够长的时间后消耗所有可用内存;至少在 Q 中,promise 的每个循环中似乎都保留了一些东西。
  • @juandopazo 在您的示例中,您应该将 i++ 更改为 ++i 否则您将获得“无限循环”。
  • 我已经成功使用了这个解决方案,甚至使用了require ('promise'); 库。我现在想知道是否可以构建非递归解决方案,请参阅stackoverflow.com/questions/36361827/…
  • Ashe,你能更具体地谈谈内存问题吗?是带包装的吗?
【解决方案3】:

现在使用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');
});

【讨论】:

  • 这太好了,是否可以用 bluebird 做到这一点,这样我就不需要同时使用 2 个不同的 Promise 库了?
【解决方案4】:

这是针对 bluebird 而不是 q 但由于您没有特别提到 q.. 在 bluebird api doc 中作者提到返回一个 promise-generating 函数比使用 deferreds 更惯用。

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

【讨论】:

  • 这很有帮助,使用递归函数来实现 while 循环。谢谢。
  • 我发现看到这一点也很有用,但我担心(供我使用 - 使用 SCAN 迭代所有 redis 键)递归会生成太多堆栈,或者对于大数据失败集,或消耗不必要的内存。我在想 es6 生成器可能是我需要走的路。
  • 这不是我想要的,但帮助我找到了解决方案。
【解决方案5】:

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

【讨论】:

    【解决方案6】:

    由于我无法评论 Stuart K 的回答,我将在此补充一点。根据 Stuart K 的回答,您可以将其归结为一个非常简单的概念:重用未实现的承诺。他所拥有的基本上是:

    1. 创建延迟承诺的新实例
    2. 定义要循环调用的函数
    3. 在该函数内部:
      1. 检查是否已完成;当你解决 #1 中创建的承诺并返回它时。
      2. 如果您还没有完成,那么告诉 Q 使用现有的 Promise 并运行未完成的函数,即“递归”函数,或者如果它死亡则失败。 Q.when(promise, yourFunction, failFunction)
    4. 定义函数后,使用 Q 首次使用 Q.nextTick(yourFunction) 触发函数
    5. 最后将你的新承诺返回给调用者(这将触发整个事情的开始)。

    Stuart 的答案是一个更通用的解决方案,但基础知识很棒(一旦你意识到它是如何工作的)。

    【讨论】:

      【解决方案7】:

      我写了一个模块,它可以帮助您使用承诺执行异步任务的链式循环,它基于 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

      【讨论】:

        【解决方案8】:

        使用 ES6 Promise,我想出了这个。它链接承诺并返回承诺。从技术上讲,它不是一个 while 循环,但确实展示了如何同步迭代 Promise。

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

        Here's my fiddle.

        【讨论】:

        • 提示:使用Promise.resolve(true) 而不是new Promise 构造函数
        【解决方案9】:
        var Q = require('q')
        
        var vetor  = ['a','b','c']
        
        function imprimeValor(elements,initValue,defer){
        
            console.log( elements[initValue++] )
            defer.resolve(initValue)
            return defer.promise
        }
        
        function Qloop(initValue, elements,defer){
        
            Q.when( imprimeValor(elements, initValue, Q.defer()), function(initValue){
        
                if(initValue===elements.length){
                    defer.resolve()
                }else{
                    defer.resolve( Qloop(initValue,elements, Q.defer()) )
                }
            }, function(err){
        
                defer.reject(err)
            })
        
            return defer.promise
        }
        
        Qloop(0, vetor,Q.defer())
        

        【讨论】:

          【解决方案10】:

          我想我还不如把我的帽子扔在戒指上,使用 ES6 Promises...

          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 参数是 the same,因为它传递给 Promise 构造函数,但会被重复调用,直到触发成功回调。 before_retry 函数允许对失败的尝试进行自定义错误处理。如果它返回一个真值,它将被视为一种成功形式,并且“循环”将结束,结果是真值。如果没有注册 before_retry 函数,或者它返回一个 false 值,则循环将运行另一个迭代。第三种选择是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
          

          【讨论】:

            【解决方案11】:

            我现在正在使用这个:

            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。为数组中的每个元素调用一次提供的函数,并传递当前元素及其在数组中的索引。它可能是同步的或异步的,在这种情况下它必须返回一个 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 函数中,您可以通过throwError(在同步函数的情况下)或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);
            })

            【讨论】:

              【解决方案12】:

              这是我发现的表达基本模式的最简单方法:定义一个调用 promise 的函数,检查其结果,然后再次调用自身或终止。

              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!'))
              

              See it in action on JSBin

              如果您使用可解析或拒绝的承诺,则应定义 thencatch 而不是使用 if 子句。

              如果您有一系列承诺,您只需将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')
              }
              

              See it in action on CodePen

              如您所见,它使用普通的 while 循环并且没有递归。

              【讨论】:

              • 非常好的解决方案!我对此进行了调整,以通过方法链将另一个变量传递给每个 Promise。感谢您提供了一个非常简单的示例!
              • 比其他答案更有帮助
              【解决方案13】:

              这里有很多答案,而您想要实现的目标并不是很实用。但这应该可行。这是在 aws lambda 函数中实现的,在 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');
                });
              }
              

              在 lambda 上测试并且运行良好超过 5 分钟。但正如其他人所说,这不是一件好事。

              【讨论】:

                【解决方案14】:

                这是一个使用 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}`));
                

                【讨论】:

                  猜你喜欢
                  • 2016-09-29
                  • 1970-01-01
                  • 2017-04-21
                  • 2015-11-13
                  • 2017-07-18
                  • 1970-01-01
                  • 2015-06-05
                  相关资源
                  最近更新 更多