web-dev-qa-db-fra.com

JavaScript ES6 promesse pour la boucle

for (let i = 0; i < 10; i++) {
    const promise = new Promise((resolve, reject) => {
        const timeout = Math.random() * 1000;
        setTimeout(() => {
            console.log(i);
        }, timeout);
    });

    // TODO: Chain this promise to the previous one (maybe without having it running?)
}

Ce qui précède donnera la sortie aléatoire suivante:

6
9
4
8
5
1
7
2
3
0

La tâche est simple: assurez-vous que chaque promesse ne s'exécute qu'après l'autre (.then()).

Pour une raison quelconque, je ne pouvais pas trouver un moyen de le faire.

J'ai essayé des fonctions de générateur (yield), des fonctions simples qui renvoient une promesse, mais en fin de compte, cela revient toujours au même problème: La boucle est synchrone .

Avec asynchrone , j'utiliserais simplement async.series().

Comment le résolvez-vous?

94
Poni

Comme vous l'avez déjà indiqué dans votre question, votre code crée toutes les promesses de manière synchrone. Au lieu de cela, ils ne devraient être créés qu'au moment de la résolution du précédent.

Deuxièmement, chaque promesse créée avec _new Promise_ doit être résolue avec un appel à resolve (ou reject). Cela devrait être fait à la fin du délai imparti. Cela déclenchera tout rappel then que vous auriez sur cette promesse. Et un tel rappel then (ou await) est une nécessité pour mettre en œuvre la chaîne.

Avec ces ingrédients, il existe plusieurs façons d'effectuer ce chaînage asynchrone:

  1. Avec une boucle for qui commence par une promesse résolue immédiatement

  2. Avec _Array#reduce_ qui commence par une promesse résolue immédiatement

  3. Avec une fonction qui se passe comme rappel de résolution

  4. Avec la syntaxe async/await d'ECMAScript2017

  5. Avec la syntaxe proposée _for await...of_ d'ECMAScript2020

Voir un extrait et des commentaires pour chacune de ces options ci-dessous.

1. Avec for

Vous pouvez utiliser une boucle for, mais vous devez vous assurer qu'elle n'exécute pas _new Promise_ de manière synchrone. Au lieu de cela, vous créez une première promesse résolue immédiatement, puis enchaînez de nouvelles promesses au fur et à mesure que les précédentes résolvent:

_for (let i = 0, p = Promise.resolve(); i < 10; i++) {
    p = p.then(_ => new Promise(resolve =>
        setTimeout(function () {
            console.log(i);
            resolve();
        }, Math.random() * 1000)
    ));
}_

2. Avec reduce

Ceci est juste une approche plus fonctionnelle de la stratégie précédente. Vous créez un tableau avec la même longueur que la chaîne que vous souhaitez exécuter et commencez avec une promesse résolue immédiatement:

_[...Array(10)].reduce( (p, _, i) => 
    p.then(_ => new Promise(resolve =>
        setTimeout(function () {
            console.log(i);
            resolve();
        }, Math.random() * 1000)
    ))
, Promise.resolve() );_

Ceci est probablement plus utile lorsque vous avez un tableau avec les données à utiliser dans les promesses.

3. Avec une fonction se passant comme résolution-callback

Ici, nous créons une fonction et l'appelons immédiatement. Cela crée la première promesse de manière synchrone. Quand elle se résout, la fonction est appelée à nouveau:

_(function loop(i) {
    if (i < 10) new Promise((resolve, reject) => {
        setTimeout( () => {
            console.log(i);
            resolve();
        }, Math.random() * 1000);
    }).then(loop.bind(null, i+1));
})(0);_

Cela crée une fonction nommée loop, et à la toute fin du code, vous pouvez voir qu’elle est appelée immédiatement avec l’argument 0. C’est le compteur, et le i argument. La fonction créera une nouvelle promesse si ce compteur est toujours inférieur à 10, sinon le chaînage s'arrête.

L'appel à resolve() déclenchera le rappel then qui appellera à nouveau la fonction. loop.bind(null, i+1) n'est qu'une façon différente de dire _ => loop(i+1).

4. Avec async/await

Les moteurs JS modernes supportent cette syntaxe :

_(async function loop() {
    for (let i = 0; i < 10; i++) {
        await new Promise(resolve => setTimeout(resolve, Math.random() * 1000));
        console.log(i);
    }
})();_

Cela peut paraître étrange, car il ressemble à celui des appels new Promise() de manière synchrone, mais en réalité la fonction async renvoie lorsqu'il exécute le premier await. Chaque fois qu'une promesse attendue est résolue, le contexte d'exécution de la fonction est restauré et continue après le await, jusqu'à ce qu'il rencontre la suivante et continue ainsi jusqu'à la fin de la boucle.

Comme il peut être courant de renvoyer une promesse en fonction d'un délai d'attente, vous pouvez créer une fonction distincte pour générer une telle promesse. Ceci s'appelle en promettant une fonction, dans ce cas setTimeout. Cela peut améliorer la lisibilité du code:

_const delay = ms => new Promise(resolve => setTimeout(resolve, ms));

(async function loop() {
    for (let i = 0; i < 10; i++) {
        await delay(Math.random() * 1000);
        console.log(i);
    }
})();_

5. Avec _for await...of_

Encore plus récemment, la syntaxe for await...of a trouvé son chemin dans certains moteurs JavaScript. Bien que cela ne réduise pas vraiment le code dans ce cas, cela permet d'isoler la définition de la chaîne d'intervalle aléatoire de l'itération réelle de celle-ci:

_const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
async function * randomDelays(count ,max) {
    for (let i = 0; i < count; i++) yield delay(Math.random() * max).then(() => i);
}

(async function loop() {
    for await (let i of randomDelays(10, 1000)) console.log(i);
})();_
220
trincot

Vous pouvez utiliser async/await pour cela. Je voudrais expliquer plus, mais il n'y a rien vraiment à cela. C'est juste une boucle for régulière, mais j'ai ajouté le mot clé await avant la construction de votre promesse.

Ce que j’aime dans cette question est que votre promesse peut résoudre une valeur normale au lieu d’avoir un effet secondaire comme votre code (ou d’autres réponses ici). Cela vous donne des pouvoirs comme dans La Légende de Zelda: un lien vers le passé où vous pouvez affecter des choses à la fois dans le Monde de Lumière et le monde des ténèbres - c’est-à-dire que vous pouvez facilement travailler avec des données avant/après que les données promises soient disponibles sans avoir à recourir à des fonctions profondément imbriquées, à d’autres structures de contrôle lourdes ou stupides - IIFE s.

// where DarkWorld is in the scary, unknown future
// where LightWorld is the world we saved from Ganondorf
LightWorld ... await DarkWorld

Alors, voici à quoi ça va ressembler ...

const someProcedure = async n =>
  {
    for (let i = 0; i < n; i++) {
      const t = Math.random() * 1000
      const x = await new Promise(r => setTimeout(r, t, i))
      console.log (i, x)
    }
    return 'done'
  }

someProcedure(10).then(x => console.log(x)) // => Promise
// 0 0
// 1 1
// 2 2
// 3 3
// 4 4
// 5 5
// 6 6
// 7 7
// 8 8
// 9 9
// done

Voyez comment nous n'avons pas à gérer cet appel gênant .then dans notre procédure? Et le mot clé async garantit automatiquement le retour d'un Promise afin que nous puissions enchaîner un appel .then sur la valeur renvoyée. Cela nous prépare à un grand succès: exécutez la séquence de n Promises, puis faites quelque chose d'important - comme afficher un message de réussite/d'erreur.

10
user633183

Sur la base de l'excellente réponse de trincot, j'ai écrit une fonction réutilisable qui accepte qu'un gestionnaire s'exécute sur chaque élément d'un tableau. La fonction elle-même renvoie une promesse qui vous permet d'attendre la fin de la boucle et que la fonction de gestionnaire que vous passez peut également renvoyer une promesse.

boucle (éléments, gestionnaire): promesse

Il m'a fallu un certain temps pour bien faire les choses, mais je pense que le code suivant sera utilisable dans de nombreuses situations de boucle de promesse.

Code prêt à copier-coller:

// SEE https://stackoverflow.com/a/46295049/286685
const loop = (arr, fn, busy, err, i=0) => {
  const body = (ok,er) => {
    try {const r = fn(arr[i], i, arr); r && r.then ? r.then(ok).catch(er) : ok(r)}
    catch(e) {er(e)}
  }
  const next = (ok,er) => () => loop(arr, fn, ok, er, ++i)
  const run  = (ok,er) => i < arr.length ? new Promise(body).then(next(ok,er)).catch(er) : ok()
  return busy ? run(busy,err) : new Promise(run)
}

Usage

Pour l'utiliser, appelez-le avec le tableau à boucler comme premier argument et le gestionnaire en tant que second. Ne transmettez pas de paramètres pour les troisième, quatrième et cinquième arguments, ils sont utilisés en interne.

const loop = (arr, fn, busy, err, i=0) => {
  const body = (ok,er) => {
    try {const r = fn(arr[i], i, arr); r && r.then ? r.then(ok).catch(er) : ok(r)}
    catch(e) {er(e)}
  }
  const next = (ok,er) => () => loop(arr, fn, ok, er, ++i)
  const run  = (ok,er) => i < arr.length ? new Promise(body).then(next(ok,er)).catch(er) : ok()
  return busy ? run(busy,err) : new Promise(run)
}

const items = ['one', 'two', 'three']

loop(items, item => {
  console.info(item)
})
.then(() => console.info('Done!'))

Cas d'utilisation avancés

Regardons la fonction de gestionnaire, les boucles imbriquées et la gestion des erreurs.

gestionnaire (courant, index, tout)

Le gestionnaire obtient 3 arguments. L'élément actuel, l'index de l'élément actuel et le tableau complet en cours de bouclage. Si la fonction de gestionnaire doit effectuer un travail asynchrone, elle peut renvoyer une promesse et la fonction de boucle attendra que la promesse soit résolue avant de commencer la prochaine itération. Vous pouvez imbriquer des appels de boucle et tout fonctionne comme prévu.

const loop = (arr, fn, busy, err, i=0) => {
  const body = (ok,er) => {
    try {const r = fn(arr[i], i, arr); r && r.then ? r.then(ok).catch(er) : ok(r)}
    catch(e) {er(e)}
  }
  const next = (ok,er) => () => loop(arr, fn, ok, er, ++i)
  const run  = (ok,er) => i < arr.length ? new Promise(body).then(next(ok,er)).catch(er) : ok()
  return busy ? run(busy,err) : new Promise(run)
}

const tests = [
  [],
  ['one', 'two'],
  ['A', 'B', 'C']
]

loop(tests, (test, idx, all) => new Promise((testNext, testFailed) => {
  console.info('Performing test ' + idx)
  return loop(test, (testCase) => {
    console.info(testCase)
  })
  .then(testNext)
  .catch(testFailed)
}))
.then(() => console.info('All tests done'))

La gestion des erreurs

De nombreux exemples de boucle de promesse que j'ai examinés ont échoué lorsqu'une exception se produit. Obtenir cette fonction pour faire le bon choix était assez délicat, mais pour autant que je sache, cela fonctionne maintenant. Assurez-vous d’ajouter un gestionnaire d’accrochement à toutes les boucles internes et d’appeler la fonction de rejet lorsque cela se produit. Par exemple.:

const loop = (arr, fn, busy, err, i=0) => {
  const body = (ok,er) => {
    try {const r = fn(arr[i], i, arr); r && r.then ? r.then(ok).catch(er) : ok(r)}
    catch(e) {er(e)}
  }
  const next = (ok,er) => () => loop(arr, fn, ok, er, ++i)
  const run  = (ok,er) => i < arr.length ? new Promise(body).then(next(ok,er)).catch(er) : ok()
  return busy ? run(busy,err) : new Promise(run)
}

const tests = [
  [],
  ['one', 'two'],
  ['A', 'B', 'C']
]

loop(tests, (test, idx, all) => new Promise((testNext, testFailed) => {
  console.info('Performing test ' + idx)
  loop(test, (testCase) => {
    if (idx == 2) throw new Error()
    console.info(testCase)
  })
  .then(testNext)
  .catch(testFailed)  //  <--- DON'T FORGET!!
}))
.then(() => console.error('Oops, test should have failed'))
.catch(e => console.info('Succesfully caught error: ', e))
.then(() => console.info('All tests done'))

UPDATE: package NPM

Depuis l’écriture de cette réponse, j’ai tourné le code ci-dessus dans un package NPM.

pour-async

Installer

npm install --save for-async

Importation

var forAsync = require('for-async');  // Common JS, or
import forAsync from 'for-async';

Utilisation (asynchrone)

var arr = ['some', 'cool', 'array'];
forAsync(arr, function(item, idx){
  return new Promise(function(resolve){
    setTimeout(function(){
      console.info(item, idx);
      // Logs 3 lines: `some 0`, `cool 1`, `array 2`
      resolve(); // <-- signals that this iteration is complete
    }, 25); // delay 25 ms to make async
  })
})

Voir le fichier readme pour plus de détails.

3
Stijn de Witt

Si vous êtes limité à ES6, la meilleure option est Promise tout. Promise.all(array) renvoie également un tableau de promesses après avoir exécuté toutes les promesses avec l'argument array. Supposons que si vous souhaitez mettre à jour de nombreux enregistrements d’étudiants dans la base de données, le code suivant illustre le concept de Promise.all dans ce cas.

let promises = [];
students.map((student, index) => {
  student.rollNo = index + 1;
  student.city = 'City Name';
  //Update whatever information on student you want
  promises.Push(student.save());
  //where save() is a function used to save data in mongoDB
});
Promise.all(promises).then(() => {
  //All the save queries will be executed when .then is executed
  //You can do further operations here after as all update operations are completed now
});

La carte est juste un exemple de méthode pour loop. Vous pouvez également utiliser la boucle for ou forin ou forEach. Le concept est donc assez simple: démarrez la boucle dans laquelle vous souhaitez effectuer des opérations asynchrones en bloc. Poussez chaque instruction d'opération async telle dans un tableau déclaré en dehors de la portée de cette boucle. Une fois la boucle terminée, exécutez l'instruction Promise all avec le tableau préparé de ces requêtes/promesses en tant qu'argument.

Le concept de base est que la boucle javascript est synchrone alors que l'appel de base de données est asynchrone et que nous utilisons la méthode Push in loop qui est également sync. Ainsi, le problème du comportement asynchrone ne se produit pas à l'intérieur de la boucle.

0
Srk95

voici mes 2 cents:

  • fonction récupérable forpromise()
  • émule un classique pour la boucle
  • permet une sortie anticipée basée sur la logique interne, renvoyant une valeur
  • peut collecter un tableau de résultats transmis dans resol/next/collect
  • défaut à commencer = 0, incrémenter = 1
  • les exceptions lancées dans la boucle sont interceptées et transmises à .catch ()
    function forpromise(lo, hi, st, res, fn) {
        if (typeof res === 'function') {
            fn = res;
            res = undefined;
        }
        if (typeof hi === 'function') {
            fn = hi;
            hi = lo;
            lo = 0;
            st = 1;
        }
        if (typeof st === 'function') {
            fn = st;
            st = 1;
        }
        return new Promise(function(resolve, reject) {

            (function loop(i) {
                if (i >= hi) return resolve(res);
                const promise = new Promise(function(nxt, brk) {
                    try {
                        fn(i, nxt, brk);
                    } catch (ouch) {
                        return reject(ouch);
                    }
                });
                promise.
                catch (function(brkres) {
                    hi = lo - st;
                    resolve(brkres)
                }).then(function(el) {
                    if (res) res.Push(el);
                    loop(i + st)
                });
            })(lo);

        });
    }


    //no result returned, just loop from 0 thru 9
    forpromise(0, 10, function(i, next) {
        console.log("iterating:", i);
        next();
    }).then(function() {


        console.log("test result 1", arguments);

        //shortform:no result returned, just loop from 0 thru 4
        forpromise(5, function(i, next) {
            console.log("counting:", i);
            next();
        }).then(function() {

            console.log("test result 2", arguments);



            //collect result array, even numbers only
            forpromise(0, 10, 2, [], function(i, collect) {
                console.log("adding item:", i);
                collect("result-" + i);
            }).then(function() {

                console.log("test result 3", arguments);

                //collect results, even numbers, break loop early with different result
                forpromise(0, 10, 2, [], function(i, collect, break_) {
                    console.log("adding item:", i);
                    if (i === 8) return break_("ending early");
                    collect("result-" + i);
                }).then(function() {

                    console.log("test result 4", arguments);

                    // collect results, but break loop on exception thrown, which we catch
                    forpromise(0, 10, 2, [], function(i, collect, break_) {
                        console.log("adding item:", i);
                        if (i === 4) throw new Error("failure inside loop");
                        collect("result-" + i);
                    }).then(function() {

                        console.log("test result 5", arguments);

                    }).
                    catch (function(err) {

                        console.log("caught in test 5:[Error ", err.message, "]");

                    });

                });

            });


        });



    });
0
cestmoi