Enquanto loop com promises

Qual seria a maneira idiomática de fazer algo como um loop while com promises? Assim:

faça alguma coisa se a condição continuar, faça-a novamente e faça outra coisa.

dosomething.then(possilblydomoresomethings).then(finish) 

Eu fiz dessa maneira, eu estava me perguntando se havia alguma maneira melhor / mais idiomática?

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

Saída: 1 2 3 4 5 6 7 8 9 10 11 feito

Eu usaria um object para envolver o valor. Dessa forma, você pode ter uma propriedade done para deixar o loop saber que está pronto.

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

Aqui está uma function reutilizável que eu acho bem clara.

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

Essa é a maneira mais simples que encontrei de expressar o padrão básico: você define uma function que chama a promise, verifica seu resultado e, em seguida, liga novamente ou termina.

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

Veja em ação no JSBin

Se você estivesse usando uma promise que resolve ou rejeita, você definiria then e catch vez de usar uma cláusula if.

Se você tivesse uma série de promises, você só mudaria o loop para mudar ou estourar o próximo a cada vez.


EDIT: Aqui está uma versão que usa async/await , porque é 2018:

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

Veja em ação no CodePen

Como você pode ver, ele usa um loop while normal e nenhuma recursion.

Isto é para bluebird não q, mas desde que você não mencionou q especificamente .. no api doc bluebird o autor menciona retornar uma function geradora de promise seria mais idiomática do que usando adiadas.

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

Como não posso comentar a resposta de Stuart K, adicionarei um pouquinho aqui. Com base na resposta de Stuart K, você pode reduzi-lo a um conceito surpreendentemente simples: Reutilizar uma promise não cumprida . O que ele tem é essencialmente:

  1. Crie uma nova instância de uma promise diferida
  2. Defina sua function que você deseja chamar em um loop
  3. Dentro dessa function:
    1. Verifique para ver se você está feito; e quando você resolver a promise criada em 1 e devolvê-la.
    2. Se você não tiver terminado, diga Q para usar a promise existente e executar a function não-preenchida que é a function “recursiva” ou falhar se ela morrer. Q.quando (promise, yourFunction, failFunction)
  4. Depois de definir sua function, use Q para acionar a function pela primeira vez usando Q.nextTick (yourFunction)
  5. Por fim, devolva sua nova promise ao chamador (o que fará com que tudo comece).

A resposta de Stuart é para uma solução mais genérica, mas o básico é incrível (depois que você percebe como funciona).

Esse padrão agora é mais facilmente chamado usando q-flow . Um exemplo, para o problema acima:

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

Aqui está uma extensão do protótipo Promise para imitar o comportamento de um loop for . Suporta promises ou valores imediatos para as seções de boot, condição, corpo de loop e incremento. Ele também tem suporte total para exceções e não possui vazamentos de memory. Um exemplo é dado abaixo sobre como usá-lo.

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

Agora estou usando isso:

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

Isso aceita um array arr e um work function e retorna um Promise . A function fornecida é chamada uma vez para cada elemento na matriz e é passada pelo elemento atual e é indexada na matriz. Pode ser sync ou async, em cujo caso deve retornar um Promise.

Você pode usá-lo assim:

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

Cada item na matriz será tratado por sua vez. Depois que todos forem manipulados, o código dado a .then() será executado ou, se algum erro ocorrer, o código dado a .catch() . Dentro da function de work , você pode throw um Error (no caso de funções síncronas) ou reject o Promise (no caso de funções assíncronas) para abortar o loop.

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

Usando a Promessa ES6, eu inventei isso. Ele prende as promises e retorna uma promise. Não é tecnicamente um loop while, mas mostra como iterar sobre promises de forma síncrona.

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

Eu pensei que poderia muito bem jogar meu chapéu no ringue, usando Promessas 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; } 

O argumento do executor é o mesmo que foi passado para um construtor Promise , mas será chamado repetidamente até triggersr o retorno de chamada bem-sucedido. A function before_retry permite o tratamento de erros customizados nas tentativas com falha. Se ele retornar um valor geral, ele será considerado uma forma de sucesso e o “loop” terminará, com essa verdade como resultado. Se nenhuma function before_retry estiver registrada, ou se retornar um valor falsey, o loop será executado para outra iteração. A terceira opção é que a function before_retry lança um erro em si. Se isso acontecer, o “loop” terminará, passando esse erro como um erro.


Aqui está um exemplo:

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

Saída para a opção 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 

Saída para a opção 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 

Saída para a opção 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 

Eu escrevi um módulo que ajuda você a fazer loops encadeados de tarefas assíncronas com promises, é baseado na resposta acima fornecida pelo 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