Résoudre les promesses les unes après les autres (c'est-à-dire en séquence)?


269

Considérez le code suivant qui lit un tableau de fichiers de manière série / séquentielle. readFilesrenvoie une promesse, qui n'est résolue qu'une fois que tous les fichiers ont été lus dans l'ordre.

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!

  });
};

Le code ci-dessus fonctionne, mais je n'aime pas avoir à faire de récursivité pour que les choses se produisent séquentiellement. Existe-t-il un moyen plus simple de réécrire ce code pour ne pas avoir à utiliser ma readSequentialfonction bizarre ?

À l'origine, j'ai essayé d'utiliser Promise.all, mais cela a provoqué tous les readFileappels simultanément, ce qui n'est pas ce que je veux:

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

2
Tout ce qui doit attendre la fin d'une opération asynchrone précédente doit être effectué lors d'un rappel. Utiliser des promesses ne change rien à cela. Vous avez donc besoin de la récursivité.
Barmar

1
Pour info, ce n'est pas techniquement récursif car il n'y a pas d'accumulation de trame de pile. Le précédent readFileSequential()est déjà retourné avant que le suivant soit appelé (car c'est asynchrone, il se termine longtemps après que l'appel de fonction d'origine est déjà revenu).
jfriend00

1
@ jfriend00 L'accumulation de trames de pile n'est pas requise pour la récursivité - uniquement une référence automatique. Mais ce n'est qu'une technicité.
Benjamin Gruenbaum

3
@BenjaminGruenbaum - mon point est qu'il n'y a absolument rien de mal à ce que la fonction s'appelle elle-même pour lancer la prochaine itération. Il n'y a aucun inconvénient et, en fait, c'est un moyen efficace de séquencer les opérations asynchrones. Il n'y a donc aucune raison d'éviter quelque chose qui ressemble à une récursivité. Il existe des solutions récursives à certains problèmes qui sont inefficaces - ce n'est pas l'un d'entre eux.
jfriend00

1
Hé, par une discussion et une demande dans la salle JavaScript, j'ai édité cette réponse afin que nous puissions pointer les autres vers elle comme canonique. Si vous n'êtes pas d'accord, veuillez me le faire savoir et je le restaurerai et en ouvrirai un autre.
Benjamin Gruenbaum

Réponses:


338

Mise à jour 2017 : j'utiliserais une fonction asynchrone si l'environnement la supporte:

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

Si vous le souhaitez, vous pouvez différer la lecture des fichiers jusqu'à ce que vous en ayez besoin à l'aide d'un générateur asynchrone (si votre environnement le prend en charge):

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

Mise à jour: À la réflexion - je pourrais utiliser une boucle for à la place:

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

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

Ou plus compact, avec réduire:

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

Dans d'autres bibliothèques de promesses (comme quand et Bluebird), vous avez des méthodes utilitaires pour cela.

Par exemple, Bluebird serait:

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

Bien qu'il n'y ait vraiment aucune raison de ne pas utiliser async, attendez aujourd'hui.


2
@ EmreTapcı, non. Le "=>" d'une fonction flèche implique déjà un retour.
Max

Si vous utilisez TypeScript, je pense que la solution de boucle "for in" est la meilleure. Réduire les retours promesses récursives par exemple. le premier type de retour d'appel est Promise <void>, puis le second est Promise <Promise <void>> et ainsi de suite - il est impossible de taper sans en utiliser je pense
Artur Tagisow

@ArturTagisow TypeScript (au moins les nouvelles versions) ont des types récursifs et devraient résoudre les types correctement ici. Il n'y a pas de promesse <Promise <T>> car les promesses "s'assimilent récursivement". Promise.resolve(Promise.resolve(15))est identique à Promise.resolve(15).
Benjamin Gruenbaum


72

Voici comment je préfère exécuter les tâches en 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 ----");
        });
}

Qu'en est-il des cas avec plus de tâches? Comme, 10?

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

8
Et qu'en est-il des cas où vous ne connaissez pas le nombre exact de tâches?
damd

1
Et qu'en est-il lorsque vous connaissez le nombre de tâches, mais uniquement au moment de l'exécution?
joeytwiddle

10
"vous ne voulez pas du tout opérer sur un tableau de promesses. Selon les spécifications de la promesse, dès qu'une promesse est créée, elle commence à s'exécuter. Donc ce que vous voulez vraiment, c'est un tableau d'usines de promesses" voir Erreur avancée n ° 3 ici: pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html
edelans

5
Si vous souhaitez réduire le bruit des lignes, vous pouvez également écrireresult = result.then(task);
Daniel Buckmaster

1
@DanielBuckmaster oui, mais attention, car si task () retourne une valeur, elle sera passée à la prochaine invocation. Si votre tâche a des arguments facultatifs, cela peut provoquer des effets secondaires. Le code actuel avale les résultats et appelle explicitement la tâche suivante sans arguments.
JHH

63

Cette question est ancienne, mais nous vivons dans un monde d'ES6 et de JavaScript fonctionnel, alors voyons comment nous pouvons nous améliorer.

Parce que les promesses s'exécutent immédiatement, nous ne pouvons pas simplement créer un tableau de promesses, elles se déclencheraient toutes en parallèle.

Au lieu de cela, nous devons créer un tableau de fonctions qui renvoie une promesse. Chaque fonction sera alors exécutée séquentiellement, ce qui commencera ensuite la promesse à l'intérieur.

Nous pouvons résoudre ce problème de plusieurs manières, mais ma façon préférée est d'utiliser reduce .

Cela devient un peu difficile à utiliser reduce en combinaison avec des promesses, j'ai donc décomposé la doublure en quelques petites piqûres digestibles ci-dessous.

L'essence de cette fonction est d'utiliser à reducepartir d'une valeur initiale dePromise.resolve([]) , ou une promesse contenant un tableau vide.

Cette promesse sera ensuite passée dans la reduceméthode as promise. C'est la clé pour enchaîner chaque promesse de manière séquentielle. La prochaine promesse à exécuter est funcet lorsque les thenincendies se produisent, les résultats sont concaténés et cette promesse est ensuite renvoyée, exécutant lereduce cycle avec la fonction de promesse suivante.

Une fois toutes les promesses exécutées, la promesse retournée contiendra un tableau de tous les résultats de chaque promesse.

Exemple ES6 (un liner)

/*
 * 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([]))

Exemple ES6 (ventilé)

// 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([]))

Usage:

// 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
très bien, merci, Array.prototype.concat.bind(result)c'est la partie qui me manquait, a dû faire pousser les résultats manuellement qui a fonctionné mais était moins cool
zavr

Puisque nous sommes tous sur JS moderne, je pense que la console.log.bind(console)déclaration dans votre dernier exemple est maintenant généralement inutile. Ces jours-ci, vous pouvez simplement passer console.log. Par exemple. serial(funcs).then(console.log). Testé sur nodejs actuel et Chrome.
Molomby

C'était un peu difficile à comprendre, mais la réduction fait essentiellement cela correctement? 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

@danecando, oui cela semble correct. Vous pouvez également supprimer Promise.resolve dans le retour, toutes les valeurs renvoyées seront automatiquement résolues, sauf si vous appelez Promise.reject dessus.
joelnet

@joelnet, en réponse au commentaire de danecando, je pense que la réduction devrait être plus correcte, exprimez-la dans l'expression suivante, êtes-vous d'accord? Promise.resolve([]).then(x => someApiCall('url1').then(r => x.concat(r))).then(x => someApiCall('url2').then(r => x.concat(r)))et ainsi de suite
bufferoverflow76

37

Pour ce faire simplement dans 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
Il semble qu'il utilise un trait de soulignement. Vous pouvez simplifier files.forEachsi les fichiers sont un tableau.
Gustavo Rodrigues

2
Eh bien ... c'est ES5. La voie ES6 serait for (file of files) {...}.
Gustavo Rodrigues

1
Vous dites que vous ne devriez pas utiliser Promise.resolve()pour créer une promesse déjà résolue dans la vie réelle. Pourquoi pas? Promise.resolve()semble plus propre que new Promise(success => success()).
canac

8
@canac Désolé, c'était juste une blague avec un jeu de mots ("promesses vides .."). Certainement utiliser Promise.resolve();dans votre code.
Shridhar Gupta

1
Belle solution, facile à suivre. Je ne suis pas le mien dans une Ci - joint la fonction, pour ainsi résoudre à la fin au lieu de mettre return sequence;je metssequence.then(() => { do stuff });
Joe Coyle

25

Utilisation simple pour la promesse standard de Node.js:

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

METTRE À JOUR

items-promise est un package NPM prêt à l'emploi faisant de même.


6
J'aimerais voir cela expliqué plus en détail.
Tyguy7

J'ai fourni une variation de cette réponse avec explication ci-dessous. Merci
Salsepareille du

C'est exactement ce que je fais dans les environnements pré-Node 7 n'ayant pas accès à async / wait. Agréable et propre.
JHH

11

J'ai dû exécuter beaucoup de tâches séquentielles et j'ai utilisé ces réponses pour forger une fonction qui se chargerait de gérer n'importe quelle tâche séquentielle ...

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

La fonction prend 2 arguments + 1 optionnel. Le premier argument est le tableau sur lequel nous allons travailler. Le deuxième argument est la tâche elle-même, une fonction qui renvoie une promesse, la tâche suivante ne sera lancée que lorsque cette promesse sera résolue. Le troisième argument est un rappel à exécuter lorsque toutes les tâches ont été effectuées. Si aucun rappel n'est passé, la fonction renvoie la promesse qu'elle a créée afin que nous puissions gérer la fin.

Voici un exemple d'utilisation:

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

J'espère que cela fera gagner du temps à quelqu'un ...


Solution incroyable, c'est la meilleure que j'ai trouvée en près d'une semaine de lutte .... Elle est très bien expliquée, a des noms internes logiques, un bon exemple (pourrait être mieux), je peux l'appeler en toute sécurité autant fois que nécessaire, et il comprend la possibilité de définir des rappels. tout simplement AGRÉABLE! (Je viens de changer le nom pour quelque chose qui me fait plus de sens) .... RECOMMANDATION pour les autres ... vous pouvez itérer un objet en utilisant 'Object.keys ( myObject )' comme votre 'objects_array'
DavidTaubmann

Merci pour votre commentaire! Je n'utilise pas non plus ce nom, mais je voulais le rendre plus évident / simple ici.
Salketer

5

La plus belle solution que j'ai pu trouver était avec des bluebirdpromesses. Vous pouvez simplement faire en Promise.resolve(files).each(fs.readFileAsync);sorte que les promesses soient résolues séquentiellement dans l'ordre.


1
Mieux encore: Promise.each(filtes, fs.readFileAsync). Btw, tu n'as pas à faire .bind(fs)?
Bergi

Personne ici ne semble comprendre la différence entre un tableau et une séquence, que celle-ci implique une taille illimitée / dynamique.
vitaly-t

Notez que les tableaux en Javascript n'ont rien à voir avec les tableaux de taille fixe dans les langages de style C. Ce ne sont que des objets avec une gestion de clé numérique boulonnée, et n'ont pas de taille ou de limite prescrite (en particulier pas lors de l'utilisation new Array(int). Tout ce qui est fait est de prédéfinir la lengthpaire clé-valeur, affectant le nombre d'indices utilisés pendant l'itération basée sur la longueur. Il a zéro effet sur l'indexation ou les limites d'index du tableau)
Mike 'Pomax' Kamermans

4

Il s'agit d'une légère variation d'une autre réponse ci-dessus. Utilisation de promesses natives:

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

Explication

Si vous avez ces tâches [t1, t2, t3], alors ce qui précède est équivalent à Promise.resolve().then(t1).then(t2).then(t3). C'est le comportement de réduire.

Comment utiliser

Tout d' abord , vous devez construire une liste de tâches! Une tâche est une fonction qui n'accepte aucun argument. Si vous devez transmettre des arguments à votre fonction, utilisez bindou d'autres méthodes pour créer une tâche. Par exemple:

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

4

Ma solution préférée:

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

Ce n'est pas fondamentalement différent des autres publiés ici, mais:

  • Applique la fonction aux articles en série
  • Résout un tableau de résultats
  • Ne nécessite pas async / wait (le support est encore assez limité, vers 2017)
  • Utilise les fonctions fléchées; agréable et concis

Exemple d'utilisation:

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

Testé sur Chrome (v59) et NodeJS (v8.1.2) actuels raisonnables.


3

Utilisez Array.prototype.reduce, et n'oubliez pas d'envelopper vos promesses dans une fonction sinon elles seront déjà en cours d'exécution!

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

agréable et facile ... vous devriez pouvoir réutiliser la même graine pour la performance, etc.

Il est important de se prémunir contre les tableaux vides ou les tableaux avec seulement 1 élément lors de l'utilisation de réduire , donc cette technique est votre meilleur pari:

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

puis appelez-le comme:

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

2

J'ai créé cette méthode simple sur l'objet Promise:

Créez et ajoutez une méthode Promise.sequence à l'objet 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();
    });
};

Usage:

var todo = [];

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

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

La meilleure chose à propos de cette extension à l'objet Promise, c'est qu'elle est cohérente avec le style des promesses. Promise.all et Promise.sequence sont invoqués de la même manière, mais ont une sémantique différente.

Mise en garde

L'exécution séquentielle des promesses n'est généralement pas un très bon moyen d'utiliser les promesses. Il est généralement préférable d'utiliser Promise.all et de laisser le navigateur exécuter le code le plus rapidement possible. Cependant, il existe de réels cas d'utilisation pour cela - par exemple lors de l'écriture d'une application mobile en utilisant javascript.


Non, vous ne pouvez pas comparer Promise.allet votre Promise.sequence. L'un prend un itérable de promesses, l'autre prend un tableau de fonctions qui renvoient des promesses.
Bergi

Au fait, je recommanderais d'éviter la promesse du constructeur contre
modèle

Je ne savais pas qu'il fallait un itérateur. Cela devrait cependant être assez facile à réécrire. Pourriez-vous expliquer pourquoi il s'agit de l'anti-modèle du constructeur de promesses? J'ai lu votre article ici: stackoverflow.com/a/25569299/1667011
frodeborli

@Bergi J'ai mis à jour le code pour prendre en charge les itérateurs. Je ne vois toujours pas qu'il s'agit d'un contre-modèle. Les antipatterns doivent généralement être considérés comme des directives pour éviter les erreurs de codage, et il est parfaitement valable de créer des fonctions (bibliothèque) qui ne respectent pas ces directives.
frodeborli

Oui, si vous le considérez comme une fonction de bibliothèque, c'est OK, mais dans ce cas, un reducecomme dans la réponse de Benjamin est juste beaucoup plus simple.
Bergi

2

Vous pouvez utiliser cette fonction qui obtient la liste promiseFactories:

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

Promise Factory est une simple fonction qui renvoie une promesse:

function myPromiseFactory() {
    return somethingThatCreatesAPromise();
}

Cela fonctionne parce qu'une fabrique de promesses ne crée pas la promesse tant qu'elle n'est pas demandée. Cela fonctionne de la même manière qu'une fonction then - en fait, c'est la même chose!

Vous ne voulez pas du tout opérer sur un éventail de promesses. Selon la spécification Promise, dès qu'une promesse est créée, elle commence à s'exécuter. Donc, ce que vous voulez vraiment, c'est un éventail d'usines prometteuses ...

Si vous voulez en savoir plus sur les promesses, vous devriez vérifier ce lien: https://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html


2

Ma réponse basée sur 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;
    });
  };

Cette solution renvoie les résultats sous forme de tableau comme Promise.all ().

Usage:

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

2

J'ai vraiment aimé la réponse de @ joelnet, mais pour moi, ce style de codage est un peu difficile à digérer, j'ai donc passé quelques jours à essayer de comprendre comment j'exprimerais la même solution de manière plus lisible et c'est ma prendre, juste avec une syntaxe différente et quelques commentaires.

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

Comme Bergi l'a remarqué, je pense que la meilleure solution et claire est d'utiliser BlueBird.each, code ci-dessous:

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

2

Tout d'abord, vous devez comprendre qu'une promesse est exécutée au moment de la création.
Ainsi, par exemple, si vous avez un code:

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

Vous devez le changer pour:

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

Ensuite, nous devons enchaîner séquentiellement les promesses:

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

en cours d'exécution after(), s'assurera que la promesse n'est créée (et exécutée) que le moment venu.


1

J'utilise le code suivant pour étendre l'objet Promise. Il gère le rejet des promesses et renvoie un tableau de résultats

Code

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

Exemple

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

Si vous le souhaitez, vous pouvez utiliser réduire pour faire une promesse séquentielle, par exemple:

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

cela fonctionnera toujours en séquence.


1

Utilisation de l'ES moderne:

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

Avec Async / Await (si vous avez le support d'ES7)

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

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

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

(vous devez utiliser la forboucle, et nonforEach parce que async / wait a des problèmes lors de l'exécution de la boucle forEach)

Sans Async / Await (en utilisant 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
Attendre à l'intérieur forEach n'est pas recommandé.
Marcelo Agimóvel

@ MarceloAgimóvel - J'ai mis à jour la solution pour ne pas travailler avec forEach(selon cela )
Gil Epshtain

0

Sur la base du titre de la question, "Résoudre les promesses l'une après l'autre (c'est-à-dire en séquence)?", Nous pourrions comprendre que le PO est plus intéressé par le traitement séquentiel des promesses lors du règlement que par les appels séquentiels en soi .

Cette réponse est offerte:

  • pour démontrer que les appels séquentiels ne sont pas nécessaires pour le traitement séquentiel des réponses.
  • pour exposer des modèles alternatifs viables aux visiteurs de cette page - y compris l'OP s'il est toujours intéressé plus d'un an plus tard.
  • malgré l'affirmation du PO selon laquelle il ne veut pas passer d'appels simultanément, ce qui peut être vraiment le cas mais peut également être une hypothèse basée sur le désir de traitement séquentiel des réponses comme le titre l'indique.

Si les appels simultanés ne sont vraiment pas souhaités, consultez la réponse de Benjamin Gruenbaum qui couvre de manière exhaustive les appels séquentiels (etc.).

Si toutefois, vous êtes intéressé (pour de meilleures performances) par des modèles qui permettent des appels simultanés suivis d'un traitement séquentiel des réponses, alors lisez la suite.

Il est tentant de penser que vous devez utiliser Promise.all(arr.map(fn)).then(fn)(comme je l'ai fait plusieurs fois) ou le sucre fantaisie d'une librairie Promise (notamment Bluebird's), cependant (avec le mérite de cet article ) un arr.map(fn).reduce(fn)modèle fera l'affaire, avec les avantages qu'il:

  • fonctionne avec n'importe quelle bibliothèque de promesses - même les versions pré-conformes de jQuery - uniquement .then() est utilisé.
  • offre la possibilité de sauter une erreur ou d'arrêter une erreur, selon ce que vous voulez avec un mod d'une ligne.

Le voici, écrit pour 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.
    });
};

Remarque: seul un fragment Q(),, est spécifique à Q. Pour jQuery, vous devez vous assurer que readFile () renvoie une promesse jQuery. Avec A + libs, les promesses étrangères seront assimilées.

La clé ici est la sequencepromesse de réduction , qui séquence le traitement des readFilepromesses mais pas leur création.

Et une fois que vous avez absorbé cela, c'est peut-être légèrement époustouflant lorsque vous réalisez que la .map()scène n'est pas vraiment nécessaire! L'ensemble du travail, les appels parallèles plus la gestion en série dans le bon ordre, peut être réalisé avec reduce()seul, plus l'avantage supplémentaire d'une flexibilité supplémentaire pour:

  • convertir des appels asynchrones parallèles en appels asynchrones série en déplaçant simplement une ligne - potentiellement utile pendant le développement.

Le voici, Qencore une fois.

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

C'est le schéma de base. Si vous vouliez également fournir des données (par exemple, les fichiers ou une transformation de ceux-ci) à l'appelant, vous auriez besoin d'une variante légère.


Je ne pense pas que ce soit une bonne idée de répondre à des questions contraires aux intentions des PO…
Bergi

1
Cette sequence.then(() => filePromise)chose est un contre-modèle - elle ne propage pas les erreurs dès qu'elles le peuvent (et crée unhandledRejectiondans les bibliothèques qui les prennent en charge). Vous devriez plutôt utiliser Q.all([sequence, filePromise])ou $.when(sequence, filePromise). Certes, ce comportement peut être ce que vous voulez lorsque vous essayez d'ignorer ou d'ignorer les erreurs, mais vous devez au moins le mentionner comme un inconvénient.
Bergi

@Bergi, j'espère que le PO interviendra et jugera si cela est vraiment contraire à ses intentions ou non. Sinon, je vais supprimer la réponse je suppose, en attendant j'espère avoir justifié ma position. Merci de le prendre suffisamment au sérieux pour fournir des commentaires décents. Pouvez-vous expliquer plus sur l'anti-modèle, ou fournir une référence s'il vous plaît? Est-ce la même chose pour l'article où j'ai trouvé le motif de base ?
Roamer-1888

1
Oui, la troisième version de son code (c'est-à-dire "à la fois parallèle et séquentielle") a le même problème. "L'antipattern" nécessite une gestion sophistiquée des erreurs et est enclin à attacher des gestionnaires de manière asynchrone, ce qui provoque des unhandledRejectionévénements. Dans Bluebird, vous pouvez contourner cela en utilisant sequence.return(filePromise)qui a le même comportement mais gère très bien les rejets. Je ne connais aucune référence, je viens juste de la trouver - je ne pense pas que le "(anti) pattern" ait encore un nom.
Bergi

1
@Bergi, vous pouvez clairement voir quelque chose que je ne peux pas :( Je me demande si ce nouvel anti-modèle doit être documenté quelque part?
Roamer-1888

0

Votre approche n'est pas mauvaise, mais elle a deux problèmes: elle avale les erreurs et elle utilise l'antipattern Explicit Promise Construction.

Vous pouvez résoudre ces deux problèmes et rendre le code plus propre, tout en utilisant la même stratégie générale:

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

Si quelqu'un d'autre a besoin d'un moyen garanti de manière séquentielle STRICTEMENT de résolution des promesses lors de l'exécution des opérations CRUD, vous pouvez également utiliser le code suivant comme base.

Tant que vous ajoutez «return» avant d'appeler chaque fonction, décrivant une promesse, et utilisez cet exemple comme base, le prochain appel de fonction .then () démarrera de manière cohérente après la fin de la précédente:

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

La méthode push and pop de tableau peut être utilisée pour la séquence de promesses. Vous pouvez également pousser de nouvelles promesses lorsque vous avez besoin de données supplémentaires. Voici le code que j'utiliserai dans React Infinite Loader pour charger une séquence de pages.

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

La plupart des réponses n'incluent pas les résultats de TOUTES les promesses individuellement, donc si quelqu'un cherche ce comportement particulier, c'est une solution possible en utilisant la récursivité.

Il suit le style de Promise.all:

  • Renvoie le tableau des résultats dans le .then()rappel.

  • Si une promesse échoue, elle est retournée immédiatement dans le .catch()rappel.

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

Remarque sur la tasksdéclaration de tableau :

Dans ce cas, il n'est pas possible d'utiliser la notation suivante comme le Promise.allferait:

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

Et nous devons utiliser:

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

La raison en est que JavaScript commence à exécuter la promesse immédiatement après sa déclaration. Si nous utilisons des méthodes comme Promise.all, il vérifie simplement que l'état de chacun d'eux est fulfilledou rejected, mais ne démarre pas l'exection elle-même. En utilisant () => promise()nous arrêtons l'exécution jusqu'à son appel.


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

Ici, la clé est de savoir comment vous appelez la fonction veille. Vous devez passer un tableau de fonctions qui renvoie lui-même une promesse au lieu d'un tableau de promesses.


-1

Il s'agit d'étendre la façon de traiter une séquence de promesses de manière plus générique, en prenant en charge des séquences dynamiques / infinies, basées sur la mise en œuvre de 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;
    });

Non seulement cette solution fonctionnera avec des séquences de n'importe quelle taille, mais vous pourrez facilement y ajouter une limitation des données et un équilibrage de charge .

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.