带有承诺的while循环
Posted
技术标签:
【中文标题】带有承诺的while循环【英文标题】:While loop with promises 【发布时间】: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 可用,则可以避免递归;请参阅下面的更新答案。 【参考方案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');
库。我现在想知道是否可以构建非递归解决方案,请参阅***.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 中创建的承诺并返回它时。
如果您还没有完成,那么告诉 Q 使用现有的 Promise 并运行未完成的函数,即“递归”函数,或者如果它死亡则失败。 Q.when(promise, yourFunction, failFunction)
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
函数中,您可以通过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);
)
【讨论】:
【参考方案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
如果您使用可解析或拒绝的承诺,则应定义 then
和 catch
而不是使用 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`));
【讨论】:
以上是关于带有承诺的while循环的主要内容,如果未能解决你的问题,请参考以下文章