While循环与承诺


74

用诺言做诸如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完成


1
“我想知道是否有更好/更多的惯用语方式?” 不,递归是要走的路。
juandopazo

没有递归怎么办?我感觉到递归可能不太酷,但是我不知道该怎么做。有什么见解吗?
Grummle

我认为,如果没有Taskjs之类的“此处停止”机制,就无法使用迭代。
juandopazo

如果您的环境中有异步/等待功能,则可以避免递归;请参阅下面的更新后的答案。
劳伦斯'18

Answers:


19

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

9
请注意,这可能会在运行足够长的时间后消耗所有可用的内存。在承诺的每个循环中似乎都保留着某些东西,至少与Q一样
。– Asherah

4
在您的示例中,@ juandopazo应该进行更改i++++i否则将出现“无限循环”。
PauloASilva,2015年

我已经成功使用了此解决方案,即使是使用require ('promise');库也是如此。现在我想知道,但如果非递归解决方案可以建立,见stackoverflow.com/questions/36361827/...
盖尔德Zamarreño

阿什(Ashe),您能否更具体地说明内存问题?有包装纸吗?
lgc_ustc

59

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

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用户而言,这可能很有用。
miguelcobain 2014年

5
新版本更具RSVP惯用性,并且在身体和状况上带有“承诺”包装:jsfiddle.net/wcW4r/3
miguelcobain 2014年

1
这会吞下异常。最好使用Q.fcall(body).then(loop,done.reject);
赞科·马捷夫(Tzanko Matev)2014年

奇怪,这给了我一个错误:错误:ReferenceError:刷新时未定义setTimeout(vendor / q.js:121:21)
Rambatino 2014年

这依赖于递归,那么它将如何扩展?尝试拨打1000个电话时会发生什么?
vitaly-t 2015年

33

这是我发现的表达基本模式的最简单方法:定义一个函数,该函数调用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!'))

观看有关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')
}

在CodePen上实际操作

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


2
非常好的解决方案!我对此进行了修改,以将另一个变量通过方法链传递给每个promise。感谢您提供一个简单的示例!
ozOli

1
比其他答案更有帮助
Omar

13

这是针对bluebird而不是q的,但是由于您没有特别提到q。.在bluebird api文档中,作者提到返回一个promise生成函数比使用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);
})

1
这很有用,使用递归函数实现while循环。谢谢。
史蒂夫·凯莱特

1
我发现查看此内容也很有用,但是我担心(对于我的使用-使用遍历所有redis键SCAN)递归会生成过多的堆栈,并且对于大型数据集会失败,或者会消耗不必要的内存。我认为es6生成器可能是我需要采用的路线。
wkw

这不是我一直在寻找的东西,但是帮助我找到了解决方案。
Gustavo Straube

5

由于我无法评论Stuart K的答案,因此在此添加一些内容。根据Stuart K的答案,您可以将其简化为一个令人惊讶的简单概念:重用未兑现的承诺。他所拥有的基本上是:

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

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


4

现在,使用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库?
特雷弗

3

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

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

1

我现在正在使用这个:

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(在同步功能的情况下)或rejectPromise(在异步功能的情况下)中止循环。


0

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

这是我的小提琴。


1
提示:使用Promise.resolve(true)而不是new Promise构造
BERGI

0

我以为我最好还是用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参数传递给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

0

这里有很多答案,您想要达到的目的也不是很实际。但这应该可行。这是在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分钟以上。但是,正如其他人所说,这不是一件好事。


-1

我编写了一个模块,该模块基于juandopazo提供的上述答案,可帮助您通过promises进行异步任务的链式循环。

/**
 * 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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.