Resolver promessas uma após a outra (ou seja, em sequência)?


269

Considere o código a seguir que lê uma matriz de arquivos de maneira serial / seqüencial. readFilesretorna uma promessa, que é resolvida apenas quando todos os arquivos foram lidos em sequência.

var readFile = function(file) {
  ... // Returns a promise.
};

var readFiles = function(files) {
  return new Promise((resolve, reject) => 

    var readSequential = function(index) {
      if (index >= files.length) {
        resolve();
      } else {
        readFile(files[index]).then(function() {
          readSequential(index + 1);
        }).catch(reject);
      }
    };

   readSequential(0); // Start!

  });
};

O código acima funciona, mas não gosto de fazer recursões para que as coisas ocorram sequencialmente. Existe uma maneira mais simples de reescrever esse código para que eu não precise usar meu códigoreadSequential função ?

Originalmente, tentei usar Promise.all, mas isso fez com que todas as readFilechamadas acontecessem simultaneamente, o que não é o que eu quero:

var readFiles = function(files) {
  return Promise.all(files.map(function(file) {
    return readFile(file);
  }));
};

2
Qualquer coisa que precise aguardar a conclusão de uma operação assíncrona anterior deve ser feita em um retorno de chamada. Usar promessas não muda isso. Então você precisa da recursão.
Barmar

1
Para sua informação, isso não é tecnicamente recursão, pois não há acúmulo de quadros de pilha. O anterior readFileSequential()já retornou antes do próximo ser chamado (por ser assíncrono, é concluído muito depois que a chamada de função original já retornou).
jfriend00

1
@ jfriend00 O acúmulo de quadros da pilha não é necessário para recursão - apenas auto-referência. Isso é apenas um detalhe técnico.
Benjamin Gruenbaum

3
@BenjaminGruenbaum - o que quero dizer é que não há absolutamente nada de errado em ter a função de se chamar para iniciar a próxima iteração. A desvantagem é zero e, de fato, é uma maneira eficiente de sequenciar operações assíncronas. Portanto, não há razão para evitar algo que se parece com recursão. Existem soluções recursivas para alguns problemas que são ineficientes - esse não é um deles.
jfriend00

1
Ei, por uma discussão e solicitação na sala JavaScript, editei esta resposta para que possamos apontar outras pessoas como canônicas. Se você não concordar, entre em contato e eu o restaurarei e abrirei outro.
Benjamin Gruenbaum

Respostas:


337

Atualização 2017 : Eu usaria uma função assíncrona se o ambiente a suportar:

async function readFiles(files) {
  for(const file of files) {
    await readFile(file);
  }
};

Se desejar, você pode adiar a leitura dos arquivos até precisar deles usando um gerador assíncrono (se o seu ambiente suportar):

async function* readFiles(files) {
  for(const file of files) {
    yield await readFile(file);
  }
};

Atualização: Pensando bem - eu poderia usar um loop for:

var readFiles = function(files) {
  var p = Promise.resolve(); // Q() in q

  files.forEach(file =>
      p = p.then(() => readFile(file)); 
  );
  return p;
};

Ou de forma mais compacta, com redução:

var readFiles = function(files) {
  return files.reduce((p, file) => {
     return p.then(() => readFile(file));
  }, Promise.resolve()); // initial
};

Em outras bibliotecas promissoras (como quando e Bluebird), você tem métodos utilitários para isso.

Por exemplo, o Bluebird seria:

var Promise = require("bluebird");
var fs = Promise.promisifyAll(require("fs"));

var readAll = Promise.resolve(files).map(fs.readFileAsync,{concurrency: 1 });
// if the order matters, you can use Promise.each instead and omit concurrency param

readAll.then(function(allFileContents){
    // do stuff to read files.
});

Embora não haja realmente nenhuma razão para não usar async, aguarde hoje.


2
@ EmreTapcı, não. A função de seta "=>" já implica retornar.
Max

Se você usa o TypeScript, acho que a solução de loop "for in" é melhor. Reduzir retornos Promessas recursivas, por exemplo. o primeiro tipo de retorno de chamada é Promise <void>, depois o segundo é Promise <Promise <void>> e assim por diante - é impossível digitar sem usar nenhum que eu penso
Artur Tagisow

O @ArturTagisow TypeScript (pelo menos novas versões) tem tipos recursivos e deve resolvê- los corretamente aqui. Não existe Promessa <Promessa <T>>, pois as promessas "assimilam-se recursivamente". Promise.resolve(Promise.resolve(15))é idêntico a Promise.resolve(15).
Benjamin Gruenbaum


72

Aqui está como eu prefiro executar tarefas em série.

function runSerial() {
    var that = this;
    // task1 is a function that returns a promise (and immediately starts executing)
    // task2 is a function that returns a promise (and immediately starts executing)
    return Promise.resolve()
        .then(function() {
            return that.task1();
        })
        .then(function() {
            return that.task2();
        })
        .then(function() {
            console.log(" ---- done ----");
        });
}

E os casos com mais tarefas? Tipo 10?

function runSerial(tasks) {
  var result = Promise.resolve();
  tasks.forEach(task => {
    result = result.then(() => task());
  });
  return result;
}

8
E os casos em que você não sabe o número exato de tarefas?
DAMD

1
E quando você sabe o número de tarefas, mas apenas em tempo de execução?
Joeytwiddle #

10
"você não deseja operar sobre uma série de promessas. De acordo com as especificações da promessa, assim que uma promessa é criada, ela começa a ser executada. Portanto, o que você realmente deseja é uma variedade de fábricas de promessas", veja Erro avançado nº 3 Aqui: pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html
edelans

5
Se você estiver em redução de ruído na linha, você também pode escreverresult = result.then(task);
Daniel Buckmaster

1
@DanielBuckmaster sim, mas tenha cuidado, pois se task () retornar um valor, ele será passado para a próxima invocação. Se sua tarefa tiver argumentos opcionais, isso poderá causar efeitos colaterais. O código atual engole os resultados e chama explicitamente a próxima tarefa sem argumentos.
JHH

63

Essa pergunta é antiga, mas vivemos em um mundo de ES6 e JavaScript funcional, então vamos ver como podemos melhorar.

Como as promessas são executadas imediatamente, não podemos apenas criar uma variedade de promessas, todas elas são disparadas em paralelo.

Em vez disso, precisamos criar uma matriz de funções que retorne uma promessa. Cada função será executada seqüencialmente, o que inicia a promessa por dentro.

Podemos resolver isso de algumas maneiras, mas minha maneira favorita é usar reduce .

Fica um pouco complicado usando reduce em combinação com promessas, então eu quebrei o liner em algumas pequenas mordidas digestíveis abaixo.

A essência desta função é usar reducecomeçando com um valor inicial dePromise.resolve([]) , ou uma promessa contendo uma matriz vazia.

Essa promessa será então passada para o reducemétodo como promise. Essa é a chave para encadear cada promessa em seqüência. A próxima promessa a executar é funce, quando os thenincêndios, os resultados são concatenados e essa promessa é retornada, executando oreduce ciclo com a próxima função de promessa.

Depois que todas as promessas forem executadas, a promessa retornada conterá uma matriz de todos os resultados de cada promessa.

Exemplo ES6 (um revestimento)

/*
 * serial executes Promises sequentially.
 * @param {funcs} An array of funcs that return promises.
 * @example
 * const urls = ['/url1', '/url2', '/url3']
 * serial(urls.map(url => () => $.ajax(url)))
 *     .then(console.log.bind(console))
 */
const serial = funcs =>
    funcs.reduce((promise, func) =>
        promise.then(result => func().then(Array.prototype.concat.bind(result))), Promise.resolve([]))

Exemplo ES6 (detalhado)

// broken down to for easier understanding

const concat = list => Array.prototype.concat.bind(list)
const promiseConcat = f => x => f().then(concat(x))
const promiseReduce = (acc, x) => acc.then(promiseConcat(x))
/*
 * serial executes Promises sequentially.
 * @param {funcs} An array of funcs that return promises.
 * @example
 * const urls = ['/url1', '/url2', '/url3']
 * serial(urls.map(url => () => $.ajax(url)))
 *     .then(console.log.bind(console))
 */
const serial = funcs => funcs.reduce(promiseReduce, Promise.resolve([]))

Uso:

// first take your work
const urls = ['/url1', '/url2', '/url3', '/url4']

// next convert each item to a function that returns a promise
const funcs = urls.map(url => () => $.ajax(url))

// execute them serially
serial(funcs)
    .then(console.log.bind(console))

1
muito bom, obrigado, Array.prototype.concat.bind(result)é a parte que estava faltando, tinha que fazer empurrando a resultados manualmente quais trabalhou, mas foi menos legal
zavr

Como todos nós somos sobre JS modernos, acredito que a console.log.bind(console)declaração em seu último exemplo agora é geralmente desnecessária. Hoje em dia você pode simplesmente passar console.log. Por exemplo. serial(funcs).then(console.log). Testado nos nodejs atuais e no Chrome.
Molomby

Isso foi um pouco difícil de entender, mas a redução está fazendo isso corretamente? Promise.resolve([]).then((x) => { const data = mockApi('/data/1'); return Promise.resolve(x.concat(data)) }).then((x) => { const data = mockApi('/data/2'); return Promise.resolve(x.concat(data)); });
Danecando 30/05

@danecando, sim, isso parece correto. Você também pode descartar o Promise.resolve no retorno, todos os valores retornados serão resolvidos automaticamente, a menos que você chame Promise.reject sobre eles.
Joelnet

@joelnet, em resposta ao comentário de danecando, acho que o que reduzir deve ser mais correto expresso na expressão a seguir, você concorda? Promise.resolve([]).then(x => someApiCall('url1').then(r => x.concat(r))).then(x => someApiCall('url2').then(r => x.concat(r)))e assim por diante
bufferoverflow76

37

Para fazer isso simplesmente no ES6:

function(files) {
  // Create a new empty promise (don't do that with real people ;)
  var sequence = Promise.resolve();

  // Loop over each file, and add on a promise to the
  // end of the 'sequence' promise.
  files.forEach(file => {

    // Chain one computation onto the sequence
    sequence = 
      sequence
        .then(() => performComputation(file))
        .then(result => doSomething(result)); 
        // Resolves for each file, one at a time.

  })

  // This will resolve after the entire chain is resolved
  return sequence;
}

1
Parece que está usando sublinhado. Você pode simplificar files.forEachse os arquivos são uma matriz.
Gustavo Rodrigues

2
Bem ... é ES5. A maneira ES6 seria for (file of files) {...}.
Gustavo Rodrigues

1
Você diz que não deve usar Promise.resolve()para criar uma promessa já resolvida na vida real. Por que não? Promise.resolve()parece mais limpo que new Promise(success => success()).
CANAC

8
@canac Desculpe, foi apenas uma piada com um jogo de palavras ("promessas vazias .."). Definitivamente use Promise.resolve();no seu código.
Shridhar Gupta

1
Solução agradável, fácil de seguir. Eu não coloque o meu em uma função, por isso, para resolver no final em vez de colocar return sequence;Coloqueisequence.then(() => { do stuff });
Joe Coyle

25

Utilitário simples para o Node.js. padrão promete:

function sequence(tasks, fn) {
    return tasks.reduce((promise, task) => promise.then(() => fn(task)), Promise.resolve());
}

ATUALIZAR

items-promessa é um pacote NPM pronto para usar, fazendo o mesmo.


6
Eu adoraria ver isso explicado em mais detalhes.
Tyguy7

Forneci uma variação dessa resposta com a explicação abaixo. Obrigado
Salsaparrilha

É exatamente isso que faço nos ambientes anteriores ao Nó 7, sem acesso ao assíncrono / aguardado. Legal e Limpo.
JHH

11

Eu tive que executar muitas tarefas seqüenciais e usei essas respostas para criar uma função que cuidaria de lidar com qualquer tarefa seqüencial ...

function one_by_one(objects_array, iterator, callback) {
    var start_promise = objects_array.reduce(function (prom, object) {
        return prom.then(function () {
            return iterator(object);
        });
    }, Promise.resolve()); // initial
    if(callback){
        start_promise.then(callback);
    }else{
        return start_promise;
    }
}

A função recebe 2 argumentos + 1 opcional. O primeiro argumento é a matriz na qual estaremos trabalhando. O segundo argumento é a tarefa em si, uma função que retorna uma promessa; a próxima tarefa será iniciada apenas quando essa promessa for resolvida. O terceiro argumento é um retorno de chamada a ser executado quando todas as tarefas estiverem concluídas. Se nenhum retorno de chamada for passado, a função retornará a promessa criada para que possamos lidar com o fim.

Aqui está um exemplo de uso:

var filenames = ['1.jpg','2.jpg','3.jpg'];
var resize_task = function(filename){
    //return promise of async resizing with filename
};
one_by_one(filenames,resize_task );

Espero que poupa alguém algum tempo ...


Solução incrível, foi a melhor que encontrei em quase uma semana de malabarismos ... Está muito bem explicado, tem nomes internos lógicos, um bom exemplo (poderia ser melhor), posso pedir por isso com segurança vezes, conforme necessário, e inclui a opção de definir retornos de chamada. simplesmente AGRADÁVEL! (Só mudou o nome para algo que me faz mais sentido) .... RECOMENDAÇÃO para os outros ... você pode iterar um objeto usando 'Object.keys ( myObject )' como sua 'objects_array'
DavidTaubmann

Obrigado por seu comentário! Também não estou usando esse nome, mas queria torná-lo mais óbvio / simples aqui.
Salketer

5

A melhor solução que consegui descobrir foi com bluebirdpromessas. Você pode fazer apenas as Promise.resolve(files).each(fs.readFileAsync);garantias de que as promessas são resolvidas sequencialmente em ordem.


1
Ainda melhor: Promise.each(filtes, fs.readFileAsync). Btw, você não tem que fazer .bind(fs)?
Bergi

Ninguém aqui parece entender a diferença entre uma matriz e uma sequência, que esta implica em tamanho ilimitado / dinâmico.
Vitaly-t

Observe que matrizes em Javascript não têm nada a ver com matrizes de tamanho fixo nas linguagens de estilo C. Eles são apenas objetos com o gerenciamento de chaves numérico ativado e não possuem tamanho ou limite prescritos ( principalmente quando usados new Array(int). Tudo o que faz é predefinir o lengthpar de valores-chave, afetando quantos índices são usados ​​durante a iteração baseada em comprimento. efeito sobre a indexação ou limites do índice)
Mike 'Pomax' Kamermans 22/02/19

4

Essa é uma pequena variação de outra resposta acima. Usando promessas nativas:

function inSequence(tasks) {
    return tasks.reduce((p, task) => p.then(task), Promise.resolve())
}

Explicação

Se você tiver essas tarefas [t1, t2, t3], o acima é equivalente a Promise.resolve().then(t1).then(t2).then(t3). É o comportamento de reduzir.

Como usar

Primeiro você precisa construir uma lista de tarefas! Uma tarefa é uma função que não aceita argumentos. Se você precisar passar argumentos para sua função, use bindou outros métodos para criar uma tarefa. Por exemplo:

var tasks = files.map(file => processFile.bind(null, file))
inSequence(tasks).then(...)

4

Minha solução preferida:

function processArray(arr, fn) {
    return arr.reduce(
        (p, v) => p.then((a) => fn(v).then(r => a.concat([r]))),
        Promise.resolve([])
    );
}

Não é fundamentalmente diferente dos outros publicados aqui, mas:

  • Aplica a função a itens em série
  • Resolve para uma matriz de resultados
  • Não requer assíncrono / espera (o suporte ainda é bastante limitado, por volta de 2017)
  • Usa funções de seta; agradável e conciso

Exemplo de uso:

const numbers = [0, 4, 20, 100];
const multiplyBy3 = (x) => new Promise(res => res(x * 3));

// Prints [ 0, 12, 60, 300 ]
processArray(numbers, multiplyBy3).then(console.log);

Testado no Chrome atual razoável (v59) e no NodeJS (v8.1.2).


3

Use Array.prototype.reducee lembre-se de agrupar suas promessas em uma função, caso contrário elas já estarão em execução!

// array of Promise providers

const providers = [
  function(){
     return Promise.resolve(1);
  },
  function(){
     return Promise.resolve(2);
  },
  function(){
     return Promise.resolve(3);
  }
]


const inSeries = function(providers){

  const seed = Promise.resolve(null); 

  return providers.reduce(function(a,b){
      return a.then(b);
  }, seed);
};

agradável e fácil ... você poderá reutilizar a mesma semente para desempenho, etc.

É importante proteger-se contra matrizes vazias ou matrizes com apenas 1 elemento ao usar reduzir , portanto, esta técnica é sua melhor aposta:

   const providers = [
      function(v){
         return Promise.resolve(v+1);
      },
      function(v){
         return Promise.resolve(v+2);
      },
      function(v){
         return Promise.resolve(v+3);
      }
    ]

    const inSeries = function(providers, initialVal){

        if(providers.length < 1){
            return Promise.resolve(null)
        }

        return providers.reduce((a,b) => a.then(b), providers.shift()(initialVal));
    };

e depois chame assim:

inSeries(providers, 1).then(v => {
   console.log(v);  // 7
});

2

Eu criei este método simples no objeto Promise:

Crie e adicione um método Promise.sequence ao objeto Promise

Promise.sequence = function (chain) {
    var results = [];
    var entries = chain;
    if (entries.entries) entries = entries.entries();
    return new Promise(function (yes, no) {
        var next = function () {
            var entry = entries.next();
            if(entry.done) yes(results);
            else {
                results.push(entry.value[1]().then(next, function() { no(results); } ));
            }
        };
        next();
    });
};

Uso:

var todo = [];

todo.push(firstPromise);
if (someCriterium) todo.push(optionalPromise);
todo.push(lastPromise);

// Invoking them
Promise.sequence(todo)
    .then(function(results) {}, function(results) {});

A melhor coisa sobre essa extensão do objeto Promise é que ela é consistente com o estilo das promessas. Promise.all e Promise.sequence são invocados da mesma maneira, mas possuem semânticas diferentes.

Cuidado

A execução seqüencial de promessas geralmente não é uma maneira muito boa de usar promessas. Geralmente é melhor usar Promise.all e deixar o navegador executar o código o mais rápido possível. No entanto, existem casos de uso reais para isso - por exemplo, ao escrever um aplicativo móvel usando javascript.


Não, você não pode comparar Promise.alle o seu Promise.sequence. Um aceita uma iterável de promessas, o outro assume uma série de funções que retornam promessas.
Bergi 29/07/2015

Btw, eu recomendo para evitar o antipattern construtor promessa
Bergi

Não sabia que era preciso um iterador. Porém, deve ser fácil reescrevê-lo. Você poderia explicar por que esse é o antipadrão do construtor de promessas? Eu fiz ler o seu post aqui: stackoverflow.com/a/25569299/1667011
frodeborli

@ Bergi Atualizei o código para suportar iteradores. Ainda não vejo que isso seja um antipadrão. Antipadrões geralmente devem ser considerados diretrizes para evitar erros de codificação, e é perfeitamente válido criar funções (de biblioteca) que quebrem essas diretrizes.
Frodeborli 30/07/2015

Sim, se você considera uma função de biblioteca, tudo bem, mas ainda assim, reducecomo na resposta de Benjamin, é muito mais simples.
Bergi 30/07/2015

2

Você pode usar esta função que é promissora

function executeSequentially(promiseFactories) {
    var result = Promise.resolve();
    promiseFactories.forEach(function (promiseFactory) {
        result = result.then(promiseFactory);
    });
    return result;
}

Promise Factory é apenas uma função simples que retorna uma Promise:

function myPromiseFactory() {
    return somethingThatCreatesAPromise();
}

Funciona porque uma fábrica de promessas não cria a promessa até que seja solicitada. Funciona da mesma maneira que uma função then - na verdade, é a mesma coisa!

Você não quer operar com uma série de promessas. De acordo com a especificação Promise, assim que uma promessa é criada, ela começa a ser executada. Então, o que você realmente quer é uma variedade de fábricas promissoras ...

Se você quiser saber mais sobre as promessas, verifique este link: https://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html


2

Minha resposta com base em https://stackoverflow.com/a/31070150/7542429 .

Promise.series = function series(arrayOfPromises) {
    var results = [];
    return arrayOfPromises.reduce(function(seriesPromise, promise) {
      return seriesPromise.then(function() {
        return promise
        .then(function(result) {
          results.push(result);
        });
      });
    }, Promise.resolve())
    .then(function() {
      return results;
    });
  };

Esta solução retorna os resultados como uma matriz como Promise.all ().

Uso:

Promise.series([array of promises])
.then(function(results) { 
  // do stuff with results here
});

2

Gostei muito da resposta da @ joelnet, mas para mim esse estilo de codificação é um pouco difícil de digerir, por isso passei alguns dias tentando descobrir como expressaria a mesma solução de uma maneira mais legível e essa é pegue, apenas com uma sintaxe diferente e alguns comentários.

// first take your work
const urls = ['/url1', '/url2', '/url3', '/url4']

// next convert each item to a function that returns a promise
const functions = urls.map((url) => {
  // For every url we return a new function
  return () => {
    return new Promise((resolve) => {
      // random wait in milliseconds
      const randomWait = parseInt((Math.random() * 1000),10)
      console.log('waiting to resolve in ms', randomWait)
      setTimeout(()=>resolve({randomWait, url}),randomWait)
    })
  }
})


const promiseReduce = (acc, next) => {
  // we wait for the accumulator to resolve it's promise
  return acc.then((accResult) => {
    // and then we return a new promise that will become
    // the new value for the accumulator
    return next().then((nextResult) => {
      // that eventually will resolve to a new array containing
      // the value of the two promises
      return accResult.concat(nextResult)
    })
  })
};
// the accumulator will always be a promise that resolves to an array
const accumulator = Promise.resolve([])

// we call reduce with the reduce function and the accumulator initial value
functions.reduce(promiseReduce, accumulator)
  .then((result) => {
    // let's display the final value here
    console.log('=== The final result ===')
    console.log(result)
  })

2

Como Bergi notou, acho que a melhor e mais clara solução é usar o BlueBird.each, código abaixo:

const BlueBird = require('bluebird');
BlueBird.each(files, fs.readFileAsync);

2

Primeiro, você precisa entender que uma promessa é executada no momento da criação.
Então, por exemplo, se você tiver um código:

["a","b","c"].map(x => returnsPromise(x))

Você precisa alterá-lo para:

["a","b","c"].map(x => () => returnsPromise(x))

Em seguida, precisamos encadear promessas sequencialmente:

["a", "b", "c"].map(x => () => returnsPromise(x))
    .reduce(
        (before, after) => before.then(_ => after()),
        Promise.resolve()
    )

execução after(), garantirá que a promessa seja criada (e executada) somente quando chegar a hora.


1

Eu uso o código a seguir para estender o objeto Promise. Ele lida com a rejeição das promessas e retorna uma variedade de resultados

Código

/*
    Runs tasks in sequence and resolves a promise upon finish

    tasks: an array of functions that return a promise upon call.
    parameters: an array of arrays corresponding to the parameters to be passed on each function call.
    context: Object to use as context to call each function. (The 'this' keyword that may be used inside the function definition)
*/
Promise.sequence = function(tasks, parameters = [], context = null) {
    return new Promise((resolve, reject)=>{

        var nextTask = tasks.splice(0,1)[0].apply(context, parameters[0]); //Dequeue and call the first task
        var output = new Array(tasks.length + 1);
        var errorFlag = false;

        tasks.forEach((task, index) => {
            nextTask = nextTask.then(r => {
                output[index] = r;
                return task.apply(context, parameters[index+1]);
            }, e=>{
                output[index] = e;
                errorFlag = true;
                return task.apply(context, parameters[index+1]);
            });
        });

        // Last task
        nextTask.then(r=>{
            output[output.length - 1] = r;
            if (errorFlag) reject(output); else resolve(output);
        })
        .catch(e=>{
            output[output.length - 1] = e;
            reject(output);
        });
    });
};

Exemplo

function functionThatReturnsAPromise(n) {
    return new Promise((resolve, reject)=>{
        //Emulating real life delays, like a web request
        setTimeout(()=>{
            resolve(n);
        }, 1000);
    });
}

var arrayOfArguments = [['a'],['b'],['c'],['d']];
var arrayOfFunctions = (new Array(4)).fill(functionThatReturnsAPromise);


Promise.sequence(arrayOfFunctions, arrayOfArguments)
.then(console.log)
.catch(console.error);

1

Se você quiser, pode usar o comando reduzir para fazer uma promessa sequencial, por exemplo:

[2,3,4,5,6,7,8,9].reduce((promises, page) => {
    return promises.then((page) => {
        console.log(page);
        return Promise.resolve(page+1);
    });
  }, Promise.resolve(1));

sempre funciona em seqüência.


1

Usando o ES moderno:

const series = async (tasks) => {
  const results = [];

  for (const task of tasks) {
    const result = await task;

    results.push(result);
  }

  return results;
};

//...

const readFiles = await series(files.map(readFile));

1

Com Async / Await (se você tiver o suporte do ES7)

function downloadFile(fileUrl) { ... } // This function return a Promise

async function main()
{
  var filesList = [...];

  for (const file of filesList) {
    await downloadFile(file);
  }
}

(você deve usar o forloop, e não forEachporque o async / waitit tenha problemas em execução no loop forEach)

Sem Async / Await (usando Promise)

function downloadFile(fileUrl) { ... } // This function return a Promise

function downloadRecursion(filesList, index)
{
  index = index || 0;
  if (index < filesList.length)
  {
    downloadFile(filesList[index]).then(function()
    {
      index++;
      downloadRecursion(filesList, index); // self invocation - recursion!
    });
  }
  else
  {
    return Promise.resolve();
  }
}

function main()
{
  var filesList = [...];
  downloadRecursion(filesList);
}

2
Aguardar dentro do forEach não é recomendado.
Marcelo Agimóvel 23/11/19

@ MarceloAgimóvel - Atualizei a solução para não funcionar forEach(de acordo com isso )
Gil Epshtain

0

Com base no título da pergunta "Resolver promessas uma após a outra (ou seja, em sequência)?", Podemos entender que o OP está mais interessado no tratamento sequencial de promessas na liquidação do que em chamadas sequenciais per se .

Esta resposta é oferecida:

  • para demonstrar que chamadas seqüenciais não são necessárias para o processamento seqüencial de respostas.
  • expor padrões alternativos viáveis ​​aos visitantes desta página - incluindo o OP, se ele ainda estiver interessado mais de um ano depois.
  • apesar da afirmação do OP de que ele não deseja fazer chamadas simultaneamente, o que pode realmente ser o caso, mas igualmente pode ser uma suposição com base no desejo de manipulação seqüencial de respostas, como o título indica.

Se chamadas simultâneas não são realmente desejadas, consulte a resposta de Benjamin Gruenbaum, que abrange chamadas sequenciais (etc) de forma abrangente.

Se, no entanto, você estiver interessado (para melhorar o desempenho) em padrões que permitam chamadas simultâneas seguidas de manipulação sequencial de respostas, continue lendo.

É tentador pensar que você deve usar Promise.all(arr.map(fn)).then(fn)(como já fiz muitas vezes) ou o açúcar sofisticado de uma lib da Promise (principalmente o Bluebird), no entanto (com crédito para este artigo ) um arr.map(fn).reduce(fn)padrão fará o trabalho, com as vantagens de:

  • funciona com qualquer promessa de lib - mesmo versões pré-compatíveis do jQuery - .then()é usado apenas .
  • oferece a flexibilidade de ignorar o erro ou parar no erro, o que você quiser com um mod de uma linha.

Aqui está, escrito para Q.

var readFiles = function(files) {
    return files.map(readFile) //Make calls in parallel.
    .reduce(function(sequence, filePromise) {
        return sequence.then(function() {
            return filePromise;
        }).then(function(file) {
            //Do stuff with file ... in the correct sequence!
        }, function(error) {
            console.log(error); //optional
            return sequence;//skip-over-error. To stop-on-error, `return error` (jQuery), or `throw  error` (Promises/A+).
        });
    }, Q()).then(function() {
        // all done.
    });
};

Nota: apenas esse fragmento,, Q()é específico para Q. Para o jQuery, você precisa garantir que readFile () retorne uma promessa do jQuery. Com A + libs, promessas estrangeiras serão assimiladas.

A chave aqui é a sequencepromessa da redução , que sequencia o tratamento das readFilepromessas, mas não a sua criação.

E depois que você absorve isso, talvez seja um pouco surpreendente quando você percebe que o .map()palco não é realmente necessário! Todo o trabalho, chamadas paralelas e manipulação em série na ordem correta, pode ser realizado reduce()sozinho, além da vantagem adicional de maior flexibilidade para:

  • converter de chamadas assíncronas paralelas para chamadas assíncronas em série movendo apenas uma linha - potencialmente útil durante o desenvolvimento.

Aqui está, Qnovamente.

var readFiles = function(files) {
    return files.reduce(function(sequence, f) {
        var filePromise = readFile(f);//Make calls in parallel. To call sequentially, move this line down one.
        return sequence.then(function() {
            return filePromise;
        }).then(function(file) {
            //Do stuff with file ... in the correct sequence!
        }, function(error) {
            console.log(error); //optional
            return sequence;//Skip over any errors. To stop-on-error, `return error` (jQuery), or `throw  error` (Promises/A+).
        });
    }, Q()).then(function() {
        // all done.
    });
};

Esse é o padrão básico. Se você também quisesse entregar dados (por exemplo, os arquivos ou algumas transformações deles) ao chamador, seria necessária uma variante moderada.


Eu não acho que é uma boa idéia para responder perguntas ao contrário das intenções PO ...
Bergi

1
Isso sequence.then(() => filePromise)é um antipadrão - ele não propaga erros assim que eles podem (e cria unhandledRejectionem bibliotecas que os suportam). Você prefere usar Q.all([sequence, filePromise])ou $.when(sequence, filePromise). É certo que esse comportamento pode ser o que você deseja quando pretende ignorar ou ignorar erros, mas você deve pelo menos mencionar isso como uma desvantagem.
Bergi 09/09/2015

@ Bergi, espero que o OP interaja e julgue se isso é realmente contrário às intenções dele ou não. Caso contrário, vou excluir a resposta, acho que, enquanto isso, espero ter justificado minha posição. Obrigado por levar a sério o suficiente para fornecer um feedback decente. Você pode explicar mais sobre o anti-padrão ou fornecer uma referência, por favor? O mesmo se aplica ao artigo em que encontrei o padrão básico ?
Roamer-1888

1
Sim, a terceira versão de seu código (que é "paralela e seqüencial") tem o mesmo problema. O "antipadrão" precisa de tratamento sofisticado de erros e é propenso a anexar manipuladores de forma assíncrona, o que causa unhandledRejectioneventos. No Bluebird, você pode contornar isso usando o sequence.return(filePromise)que tem o mesmo comportamento, mas lida bem com as rejeições. Não conheço nenhuma referência, apenas a inventei - ainda não acho que o "(anti) padrão" tenha um nome.
Bergi 09/09/2015

1
@Bergi, você pode ver claramente algo que não posso :( Gostaria de saber se esta nova necessidades anti-padrão a ser documentado em algum lugar?
Roamer-1888

0

Sua abordagem não é ruim, mas possui dois problemas: engole erros e emprega o Antipadrão de construção de promessa explícita.

Você pode resolver esses dois problemas e tornar o código mais limpo, enquanto ainda emprega a mesma estratégia geral:

var Q = require("q");

var readFile = function(file) {
  ... // Returns a promise.
};

var readFiles = function(files) {
  var readSequential = function(index) {
    if (index < files.length) {
      return readFile(files[index]).then(function() {
        return readSequential(index + 1);
      });
    }
  };

  // using Promise.resolve() here in case files.length is 0
  return Promise.resolve(readSequential(0)); // Start!
};

0

Se alguém precisar de uma maneira garantida de maneira estritamente sequencial de resolver o Promises ao executar operações CRUD, você também poderá usar o seguinte código como base.

Desde que você adicione 'return' antes de chamar cada função, descrevendo uma Promessa e use este exemplo como base, a próxima chamada de função .then () começará CONSISTENTEMENTE após a conclusão da anterior:

getRidOfOlderShoutsPromise = () => {
    return readShoutsPromise('BEFORE')
    .then(() => {
        return deleteOlderShoutsPromise();
    })
    .then(() => {
        return readShoutsPromise('AFTER')
    })
    .catch(err => console.log(err.message));
}

deleteOlderShoutsPromise = () => {
    return new Promise ( (resolve, reject) => {
        console.log("in deleteOlderShouts");
        let d = new Date();
        let TwoMinuteAgo = d - 1000 * 90 ;
        All_Shouts.deleteMany({ dateTime: {$lt: TwoMinuteAgo}}, function(err) {
            if (err) reject();
            console.log("DELETED OLDs at "+d);
            resolve();        
        });
    });
}

readShoutsPromise = (tex) => {
    return new Promise( (resolve, reject) => {
        console.log("in readShoutsPromise -"+tex);
        All_Shouts
        .find({})
        .sort([['dateTime', 'ascending']])
        .exec(function (err, data){
            if (err) reject();
            let d = new Date();
            console.log("shouts "+tex+" delete PROMISE = "+data.length +"; date ="+d);
            resolve(data);
        });    
    });
}

0

O método push e pop da matriz pode ser usado para a sequência de promessas. Você também pode enviar novas promessas quando precisar de dados adicionais. Este é o código que usarei no carregador React Infinite para carregar a sequência de páginas.

var promises = [Promise.resolve()];

function methodThatReturnsAPromise(page) {
	return new Promise((resolve, reject) => {
		setTimeout(() => {
			console.log(`Resolve-${page}! ${new Date()} `);
			resolve();
		}, 1000);
	});
}

function pushPromise(page) {
	promises.push(promises.pop().then(function () {
		return methodThatReturnsAPromise(page)
	}));
}

pushPromise(1);
pushPromise(2);
pushPromise(3);


0

A maioria das respostas não inclui os resultados de TODAS as promessas individualmente, portanto, caso alguém esteja procurando esse comportamento específico, essa é uma solução possível usando a recursão.

Segue o estilo de Promise.all:

  • Retorna a matriz de resultados no .then()retorno de chamada.

  • Se alguma promessa falhar, ela retornará imediatamente no .catch()retorno de chamada.

const promiseEach = (arrayOfTasks) => {
  let results = []
  return new Promise((resolve, reject) => {
    const resolveNext = (arrayOfTasks) => {
      // If all tasks are already resolved, return the final array of results
      if (arrayOfTasks.length === 0) return resolve(results)

      // Extract first promise and solve it
      const first = arrayOfTasks.shift()

      first().then((res) => {
        results.push(res)
        resolveNext(arrayOfTasks)
      }).catch((err) => {
        reject(err)
      })
    }
    resolveNext(arrayOfTasks)
  })
}

// Lets try it 😎

const promise = (time, shouldThrowError) => new Promise((resolve, reject) => {
  const timeInMs = time * 1000
  setTimeout(()=>{
    console.log(`Waited ${time} secs`)
    if (shouldThrowError) reject(new Error('Promise failed'))
    resolve(time)
  }, timeInMs)
})

const tasks = [() => promise(1), () => promise(2)]

promiseEach(tasks)
  .then((res) => {
    console.log(res) // [1, 2]
  })
  // Oops some promise failed
  .catch((error) => {
    console.log(error)
  })

Nota sobre a tasksdeclaração do array :

Neste caso, não é possível usar a seguinte notação como Promise.allusaria:

const tasks = [promise(1), promise(2)]

E nós temos que usar:

const tasks = [() => promise(1), () => promise(2)]

O motivo é que o JavaScript começa a executar a promessa imediatamente após sua declaração. Se usarmos métodos como Promise.allesse, apenas verifica se o estado de todos eles é fulfilledou rejected, mas não inicia a própria exceção. Usando () => promise(), paramos a execução até que seja chamada.


0
(function() {
  function sleep(ms) {
    return new Promise(function(resolve) {
      setTimeout(function() {
        return resolve();
      }, ms);
    });
  }

  function serial(arr, index, results) {
    if (index == arr.length) {
      return Promise.resolve(results);
    }
    return new Promise(function(resolve, reject) {
      if (!index) {
        index = 0;
        results = [];
      }
      return arr[index]()
        .then(function(d) {
          return resolve(d);
        })
        .catch(function(err) {
          return reject(err);
        });
    })
      .then(function(result) {
        console.log("here");
        results.push(result);
        return serial(arr, index + 1, results);
      })
      .catch(function(err) {
        throw err;
      });
  }

  const a = [5000, 5000, 5000];

  serial(a.map(x => () => sleep(x)));
})();

Aqui, a chave é como você chama a função de suspensão. Você precisa passar uma matriz de funções que retorna uma promessa em vez de uma matriz de promessas.


-1

Isso é para estender como processar uma sequência de promessas de maneira mais genérica, suportando sequências dinâmicas / infinitas, com base na implementação do spex.sequence :

var $q = require("q");
var spex = require('spex')($q);

var files = []; // any dynamic source of files;

var readFile = function (file) {
    // returns a promise;
};

function source(index) {
    if (index < files.length) {
        return readFile(files[index]);
    }
}

function dest(index, data) {
    // data = resolved data from readFile;
}

spex.sequence(source, dest)
    .then(function (data) {
        // finished the sequence;
    })
    .catch(function (error) {
        // error;
    });

Não apenas essa solução funcionará com sequências de qualquer tamanho, mas você pode adicionar facilmente otimização de dados e balanceamento de carga .

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.