Maneira correta de escrever loops para promessa.

116

Como construir corretamente um loop para garantir que a seguinte chamada de promessa e o logger.log (res) encadeado sejam executados de forma síncrona por meio da iteração? (pássaro azul)

db.getUser(email).then(function(res) { logger.log(res); }); // this is a promise

Tentei da seguinte maneira (método de http://blog.victorquinn.com/javascript-promise-while-loop )

var Promise = require('bluebird');

var promiseWhile = function(condition, action) {
    var resolver = Promise.defer();

    var loop = function() {
        if (!condition()) return resolver.resolve();
        return Promise.cast(action())
            .then(loop)
            .catch(resolver.reject);
    };

    process.nextTick(loop);

    return resolver.promise;
});

var count = 0;
promiseWhile(function() {
    return count < 10;
}, function() {
    return new Promise(function(resolve, reject) {
        db.getUser(email)
          .then(function(res) { 
              logger.log(res); 
              count++;
              resolve();
          });
    }); 
}).then(function() {
    console.log('all done');
}); 

Embora pareça funcionar, mas não acho que garanta a ordem de chamar logger.log (res);

Alguma sugestão?

user2127480
fonte
1
O código parece bom para mim (recursão com a loopfunção é a maneira de fazer loops síncronos). Por que você acha que não há garantia?
hugomg
db.getUser (email) tem a garantia de ser chamado em ordem. Mas, uma vez que db.getUser () em si é uma promessa, chamá-lo sequencialmente não significa necessariamente que as consultas de banco de dados para 'email' sejam executadas sequencialmente devido ao recurso assíncrono da promessa. Portanto, o logger.log (res) é chamado dependendo de qual consulta terminar primeiro.
user2127480
1
@ user2127480: Mas a próxima iteração do loop é chamada sequencialmente somente depois que a promessa foi resolvida, é assim que o whilecódigo funciona?
Bergi

Respostas:

78

Não acho que isso garanta a ordem de chamada de logger.log (res);

Na verdade, sim. Essa instrução é executada antes da resolvechamada.

Alguma sugestão?

Grande quantidade. O mais importante é o uso do antipadrão criar-promessa-manualmente - basta fazer apenas

promiseWhile(…, function() {
    return db.getUser(email)
             .then(function(res) { 
                 logger.log(res); 
                 count++;
             });
})…

Em segundo lugar, essa whilefunção poderia ser muito simplificada:

var promiseWhile = Promise.method(function(condition, action) {
    if (!condition()) return;
    return action().then(promiseWhile.bind(null, condition, action));
});

Terceiro, eu não usaria um whileloop (com uma variável de fechamento), mas um forloop:

var promiseFor = Promise.method(function(condition, action, value) {
    if (!condition(value)) return value;
    return action(value).then(promiseFor.bind(null, condition, action));
});

promiseFor(function(count) {
    return count < 10;
}, function(count) {
    return db.getUser(email)
             .then(function(res) { 
                 logger.log(res); 
                 return ++count;
             });
}, 0).then(console.log.bind(console, 'all done'));
Bergi
fonte
2
Opa. Exceto que actionleva valuecomo argumento em promiseFor. ASSIM, não me deixaria fazer uma edição tão pequena. Obrigado, é muito útil e elegante.
Gordon
1
@ Roamer-1888: Talvez a terminologia seja um pouco estranha, mas quero dizer que um whileloop testa algum estado global enquanto um forloop tem sua variável de iteração (contador) ligada ao próprio corpo do loop. Na verdade, usei uma abordagem mais funcional que se parece mais com uma iteração de fixpoint do que com um loop. Verifique o código novamente, o valueparâmetro é diferente.
Bergi
2
OK, eu vejo agora. À medida que .bind()ofusca o novo value, acho que posso escolher estender a função para facilitar a leitura. E desculpe se eu estou sendo estúpido, mas se coexistir promiseFore promiseWhilenão coexistir, então como um chama o outro?
Roamer-1888
2
@herve Você pode basicamente omitir e substituir return …por return Promise.resolve(…). Se você precisar de proteções adicionais contra conditionou actionlançar uma exceção (como Promise.methodfornece ), envolva todo o corpo da função em umreturn Promise.resolve().then(() => { … })
Bergi
2
@herve Na verdade, deveria ser Promise.resolve().then(action).…ou Promise.resolve(action()).…, você não precisa quebrar o valor de retorno dethen
Bergi
134

Se você realmente deseja uma promiseWhen()função geral para este e outros propósitos, então faça-o, usando as simplificações de Bergi. No entanto, por causa da maneira como as promessas funcionam, passar callbacks dessa forma geralmente é desnecessário e força você a pular pequenos obstáculos complexos.

Pelo que eu posso dizer, você está tentando:

  • para buscar de forma assíncrona uma série de detalhes do usuário para uma coleção de endereços de e-mail (pelo menos, esse é o único cenário que faz sentido).
  • para fazer isso criando uma .then()cadeia por meio de recursão.
  • para manter a ordem original ao lidar com os resultados retornados.

Definido assim, o problema é na verdade aquele discutido em "The Collection Kerfuffle" em Promise Anti-patterns , que oferece duas soluções simples:

  • chamadas assíncronas paralelas usando Array.prototype.map()
  • chamadas seriais assíncronas usando Array.prototype.reduce().

A abordagem paralela fornecerá (de maneira direta) o problema que você está tentando evitar - que a ordem das respostas é incerta. A abordagem serial construirá a .then()cadeia necessária - plana - sem recursão.

function fetchUserDetails(arr) {
    return arr.reduce(function(promise, email) {
        return promise.then(function() {
            return db.getUser(email).done(function(res) {
                logger.log(res);
            });
        });
    }, Promise.resolve());
}

Ligue da seguinte forma:

//Compose here, by whatever means, an array of email addresses.
var arrayOfEmailAddys = [...];

fetchUserDetails(arrayOfEmailAddys).then(function() {
    console.log('all done');
});

Como você pode ver, não há necessidade do feio var externo countou de sua conditionfunção associada . O limite (de 10 na questão) é determinado inteiramente pelo comprimento da matriz arrayOfEmailAddys.

Roamer-1888
fonte
16
parece que esta deve ser a resposta selecionada. abordagem elegante e muito reutilizável.
ken
1
Alguém sabe se uma captura se propagaria de volta para o pai? Por exemplo, se db.getUser falhar, o erro (rejeitar) propagará o backup?
caminho do futuro,
@wayofthefuture, no. Pense desta forma ... você não pode mudar a história.
Roamer-1888,
4
Obrigado pela resposta. Esta deve ser a resposta aceita.
klvs
1
@ Roamer-1888 Erro meu, interpretei mal a pergunta original. Eu (pessoalmente) estava procurando uma solução em que a lista inicial de que você precisa para reduzir está crescendo à medida que suas solicitações são resolvidas (é uma consulta mais de um banco de dados). Nesse caso, achei a ideia de usar a redução com um gerador uma separação bastante boa de (1) a extensão condicional da cadeia de promessa e (2) o consumo dos resultados retornados.
jhp de
40

Veja como faço isso com o objeto Promessa padrão.

// Given async function sayHi
function sayHi() {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log('Hi');
      resolve();
    }, 3000);
  });
}

// And an array of async functions to loop through
const asyncArray = [sayHi, sayHi, sayHi];

// We create the start of a promise chain
let chain = Promise.resolve();

// And append each function in the array to the promise chain
for (const func of asyncArray) {
  chain = chain.then(func);
}

// Output:
// Hi
// Hi (After 3 seconds)
// Hi (After 3 more seconds)
Youngwerth
fonte
Ótima resposta @youngwerth
Jam Risser
3
como enviar params desta forma?
Akash khan
4
@khan na linha chain = chain.then (func), você poderia fazer: chain = chain.then(func.bind(null, "...your params here")); ou chain = chain.then(() => func("your params here"));
youngwerth
9

Dado

  • função asyncFn
  • matriz de itens

Requeridos

  • encadeamento de promessas. então () 's em série (em ordem)
  • es6 nativo

Solução

let asyncFn = (item) => {
  return new Promise((resolve, reject) => {
    setTimeout( () => {console.log(item); resolve(true)}, 1000 )
  })
}

// asyncFn('a')
// .then(()=>{return async('b')})
// .then(()=>{return async('c')})
// .then(()=>{return async('d')})

let a = ['a','b','c','d']

a.reduce((previous, current, index, array) => {
  return previous                                    // initiates the promise chain
  .then(()=>{return asyncFn(array[index])})      //adds .then() promise for each item
}, Promise.resolve())
Kamran
fonte
2
Se asyncestá prestes a se tornar uma palavra reservada em JavaScript, pode ser mais claro renomear essa função aqui.
hippietrail
Além disso, não é o caso de que as funções de seta gorda sem um corpo entre colchetes simplesmente retornem o que a expressão ali avalia? Isso tornaria o código mais conciso. Eu também posso adicionar um comentário informando que currentnão é usado.
hippietrail
2
esta é a maneira correta!
teleme.io
3

A função sugerida por Bergi é muito boa:

var promiseWhile = Promise.method(function(condition, action) {
      if (!condition()) return;
    return action().then(promiseWhile.bind(null, condition, action));
});

Ainda assim, quero fazer uma pequena adição, que faz sentido, ao usar promessas:

var promiseWhile = Promise.method(function(condition, action, lastValue) {
  if (!condition()) return lastValue;
  return action().then(promiseWhile.bind(null, condition, action));
});

Desta forma, o loop while pode ser embutido em uma cadeia de promessa e resolvido com lastValue (também se a ação () nunca for executada). Consultar exemplo:

var count = 10;
util.promiseWhile(
  function condition() {
    return count > 0;
  },
  function action() {
    return new Promise(function(resolve, reject) {
      count = count - 1;
      resolve(count)
    })
  },
  count)
Patrick Wieth
fonte
3

Eu faria algo assim:

var request = []
while(count<10){
   request.push(db.getUser(email).then(function(res) { return res; }));
   count++
};

Promise.all(request).then((dataAll)=>{
  for (var i = 0; i < dataAll.length; i++) {

      logger.log(dataAll[i]); 
  }  
});

dessa forma, dataAll é uma matriz ordenada de todos os elementos a serem registrados. E a operação de log será executada quando todas as promessas forem cumpridas.

Claudio
fonte
Promise.all chamará o will call promises ao mesmo tempo. Portanto, a ordem de conclusão pode mudar. A pergunta pede promessas acorrentadas. Portanto, a ordem de conclusão não deve ser alterada.
canbax de
Editar 1: Você não precisa chamar Promise.all. Enquanto as promessas forem cumpridas, elas serão executadas em paralelo.
canbax de
1

Use async e await (es6):

function taskAsync(paramets){
 return new Promise((reslove,reject)=>{
 //your logic after reslove(respoce) or reject(error)
})
}

async function fName(){
let arry=['list of items'];
  for(var i=0;i<arry.length;i++){
   let result=await(taskAsync('parameters'));
}

}
ramachandrareddy reddam
fonte
0
function promiseLoop(promiseFunc, paramsGetter, conditionChecker, eachFunc, delay) {
    function callNext() {
        return promiseFunc.apply(null, paramsGetter())
            .then(eachFunc)
    }

    function loop(promise, fn) {
        if (delay) {
            return new Promise(function(resolve) {
                setTimeout(function() {
                    resolve();
                }, delay);
            })
                .then(function() {
                    return promise
                        .then(fn)
                        .then(function(condition) {
                            if (!condition) {
                                return true;
                            }
                            return loop(callNext(), fn)
                        })
                });
        }
        return promise
            .then(fn)
            .then(function(condition) {
                if (!condition) {
                    return true;
                }
                return loop(callNext(), fn)
            })
    }

    return loop(callNext(), conditionChecker);
}


function makeRequest(param) {
    return new Promise(function(resolve, reject) {
        var req = https.request(function(res) {
            var data = '';
            res.on('data', function (chunk) {
                data += chunk;
            });
            res.on('end', function () {
                resolve(data);
            });
        });
        req.on('error', function(e) {
            reject(e);
        });
        req.write(param);
        req.end();
    })
}

function getSomething() {
    var param = 0;

    var limit = 10;

    var results = [];

    function paramGetter() {
        return [param];
    }
    function conditionChecker() {
        return param <= limit;
    }
    function callback(result) {
        results.push(result);
        param++;
    }

    return promiseLoop(makeRequest, paramGetter, conditionChecker, callback)
        .then(function() {
            return results;
        });
}

getSomething().then(function(res) {
    console.log('results', res);
}).catch(function(err) {
    console.log('some error along the way', err);
});
Tengiz
fonte
0

Que tal este usando BlueBird ?

function fetchUserDetails(arr) {
    return Promise.each(arr, function(email) {
        return db.getUser(email).done(function(res) {
            logger.log(res);
        });
    });
}
caminho do futuro
fonte
0

Aqui está outro método (ES6 w / std Promise). Usa critérios de saída do tipo lodash / sublinhado (return === false). Observe que você pode facilmente adicionar um método exitIf () nas opções para executar em doOne ().

const whilePromise = (fnReturningPromise,options = {}) => { 
    // loop until fnReturningPromise() === false
    // options.delay - setTimeout ms (set to 0 for 1 tick to make non-blocking)
    return new Promise((resolve,reject) => {
        const doOne = () => {
            fnReturningPromise()
            .then((...args) => {
                if (args.length && args[0] === false) {
                    resolve(...args);
                } else {
                    iterate();
                }
            })
        };
        const iterate = () => {
            if (options.delay !== undefined) {
                setTimeout(doOne,options.delay);
            } else {
                doOne();
            }
        }
        Promise.resolve()
        .then(iterate)
        .catch(reject)
    })
};
GrumpyGary
fonte
0

Usar o objeto de promessa padrão e fazer com que a promessa retorne os resultados.

function promiseMap (data, f) {
  const reducer = (promise, x) =>
    promise.then(acc => f(x).then(y => acc.push(y) && acc))
  return data.reduce(reducer, Promise.resolve([]))
}

var emails = []

function getUser(email) {
  return db.getUser(email)
}

promiseMap(emails, getUser).then(emails => {
  console.log(emails)
})
Chris Blaser
fonte
0

Primeiro pegue o array de promessas (array de promessa) e depois resolva esse array de promessa usando Promise.all(promisearray).

var arry=['raju','ram','abdul','kruthika'];

var promiseArry=[];
for(var i=0;i<arry.length;i++) {
  promiseArry.push(dbFechFun(arry[i]));
}

Promise.all(promiseArry)
  .then((result) => {
    console.log(result);
  })
  .catch((error) => {
     console.log(error);
  });

function dbFetchFun(name) {
  // we need to return a  promise
  return db.find({name:name}); // any db operation we can write hear
}
ramachandrareddy reddam
fonte