175 votes

JavaScript ES6 promesse pour 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?)
}

L'opération ci-dessus donnera la sortie aléatoire suivante :

6
9
4
8
5
1
7
2
3
0

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

Pour une raison quelconque, je n'ai pas trouvé le moyen de le faire.

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

Avec asynchrone J'utiliserais simplement async.series() .

Comment le résoudre ?

0 votes

449voto

trincot Points 10112

Comme vous l'avez déjà laissé entendre dans votre question, votre code crée toutes les promesses de manière synchrone. Au lieu de cela, elles ne devraient être créées qu'au moment où la précédente est résolue.

Deuxièmement, chaque promesse qui est créée avec new Promise doit être résolu par un appel à resolve (ou reject ). Cela doit être fait à l'expiration de la minuterie. Cela déclenchera tout then callback que vous auriez sur cette promesse. Et une telle then (ou await ) est une nécessité pour la mise en œuvre de la chaîne.

Avec ces ingrédients, il existe plusieurs façons d'effectuer cet enchaînement asynchrone :

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

  2. Avec Array#reduce qui commence par une promesse immédiatement résoluble

  3. Avec une fonction qui se passe elle-même comme callback de résolution

  4. Avec l'option de l'ECMAScript2017 async / await syntaxe

  5. Avec la méthode ECMAScript2020 for await...of syntaxe

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

1. Avec for

Vous peut utiliser un 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 promesse initiale à résolution immédiate, puis vous enchaînez de nouvelles promesses à mesure que les précédentes se 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

Il s'agit simplement d'une approche plus fonctionnelle de la stratégie précédente. Vous créez un tableau de la même longueur que la chaîne que vous voulez exécuter, et vous commencez avec une promesse à résolution immédiate :

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

C'est probablement plus utile lorsque vous ont un tableau avec les données à utiliser dans les promesses.

3. Avec une fonction se passant elle-même comme résolution-callback

Ici, nous créons une fonction et l'appelons immédiatement. Elle crée la première promesse de manière synchrone. Lorsqu'elle est résolue, 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'il est appelé immédiatement avec l'argument 0. C'est le compteur, et la fonction i argument. La fonction créera une nouvelle promesse si ce compteur est toujours inférieur à 10, sinon l'enchaînement s'arrête.

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

4. Avec async / await

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 sembler étrange, car il semble comme le new Promise() sont exécutés de manière synchrone, mais en réalité, les appels de la async fonction 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 la fonction poursuit son chemin après le processus de résolution de la promesse. await jusqu'à ce qu'il rencontre le suivant, et ainsi de suite jusqu'à la fin de la boucle.

Comme il peut être courant de renvoyer une promesse basée sur un délai d'attente, vous pouvez créer une fonction distincte pour générer une telle promesse. Cette fonction s'appelle 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

Avec EcmaScript 2020, le for await...of a trouvé sa place dans les moteurs JavaScript modernes. Bien qu'elle ne réduise pas vraiment le code dans ce cas, elle permet d'isoler la définition de la chaîne d'intervalles aléatoires de son itération réelle :

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

62 votes

Je me demande juste comment ce type a pu trouver autant de solutions.

0 votes

La dernière méthode de est parfaite, nous devons juste ajouter async avec la fonction externe et await avec Promises à l'intérieur de la boucle

1 votes

@trincot merci beaucoup ! Cette réponse m'a énormément aidé à mieux comprendre les promesses.

17voto

naomik Points 10423

Vous pouvez utiliser async/await pour ça. J'en dirais plus, mais il n'y a rien de vraiment important. C'est juste un for mais j'ai ajouté la await mot-clé avant la construction de votre promesse

Ce que j'aime dans cette méthode, c'est que votre Promise peut résoudre une valeur normale au lieu d'avoir un effet secondaire comme votre code (ou d'autres réponses ici) l'incluent. Cela vous donne des pouvoirs comme dans The Legend of Zelda : A Link to the Past (en anglais) où vous pouvez agir sur les choses dans le monde de la 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 difficiles à manier ou à des méthodes stupides. IIFE s.

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

Voici donc à quoi cela va ressembler...

async function someProcedure (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(console.log)
  .catch(console.error)

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 à faire face à cette ennuyeuse .then dans notre procédure ? Et async garantit automatiquement qu'une Promise est retourné, nous pouvons donc enchaîner un .then sur la valeur retournée. Cela nous permet d'obtenir un grand succès : exécuter la séquence de n Des promesses, puis faire quelque chose d'important - comme afficher un message de réussite ou d'erreur.

0 votes

Attendre la nouvelle Promesse n'est pas valide

3 votes

@AndroidDev Je ne sais pas s'il s'agit d'une violation de la syntaxe ecmascript, mais ça fonctionne ici, dans Chrome 58 - des parenthèses comme await (expr) peut être utilisé pour résoudre l'ambiguïté autrement. J'ai mis à jour la question pour inclure un extrait de code fonctionnel.

2 votes

@AndroidDev, votre affirmation est fausse.

9voto

Stijn de Witt Points 3515

En me basant sur l'excellente réponse de trincot, j'ai écrit une fonction réutilisable qui accepte un handler à exécuter 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 la fonction de gestion que vous passez peut également renvoyer une promesse.

loop(items, handler) : Promise

Il m'a fallu du temps pour y arriver, mais je pense que le code suivant sera utilisable dans de nombreuses situations de rupture de promesse.

Copier-coller du code prêt :

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

Utilisation

Pour l'utiliser, appelez-la avec le tableau à boucler comme premier argument et la fonction de gestion comme deuxième. Ne pas passer 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ée

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

handler(current, index, all)

Le gestionnaire reçoit 3 arguments. L'élément courant, l'index de l'élément courant et le tableau complet sur lequel on boucle. Si la fonction de gestion doit effectuer un travail asynchrone, elle peut renvoyer une promesse et la fonction de boucle attendra que la promesse soit résolue avant de lancer l'itération suivante. Vous pouvez imbriquer les invocations 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'))

Traitement des erreurs

Beaucoup d'exemples de promesse-looping que j'ai examinés s'effondrent lorsqu'une exception se produit. Obtenir que cette fonction fasse la bonne chose a été assez délicat, mais pour autant que je puisse dire, elle fonctionne maintenant. Assurez-vous d'ajouter un gestionnaire de capture à toutes les boucles internes et invoquez la fonction de rejet lorsqu'elle 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 : paquet NPM

Depuis que j'ai écrit cette réponse, j'ai transformé le code ci-dessus en un paquet NPM.

pour-asynchrone

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

Consultez le fichier readme du paquet pour plus de détails.

0 votes

Il serait bon d'ajouter quelques commentaires à la fonction de boucle .

2 votes

@kofifus Oui, vous avez raison. Depuis que j'ai écrit cette réponse, j'ai transformé ce code en un projet NPM documenté. Je vais ajouter le lien vers la réponse.

0 votes

Ok le code là semble totalement différent de celui-ci ...

2voto

Srk95 Points 26

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

let promises = students.map((student, index) => {
//where students is a db object
student.rollNo = index + 1;
student.city = 'City Name';
//Update whatever information on student you want
return student.save();
});
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
});

Map est juste une méthode d'exemple pour la boucle. Vous pouvez également utiliser for ou forin ou forEach boucle. Le concept est donc assez simple : lancez la boucle dans laquelle vous voulez effectuer des opérations asynchrones en masse. Placez chaque déclaration d'opération asynchrone 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 comme argument.

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

0 votes

Pourquoi utiliser map si vous le traitez juste comme un forEach et ne va pas stocker les résultats ? Pourquoi ne pas assigner la carte à promises et éviter les push à l'intérieur ?

4 votes

L'exigence de l'OP était de "...Assurez-vous que chaque promesse ne fonctionne qu'après l'autre..." .

0 votes

@vol7ron J'admets que je suis aussi coupable d'utiliser map i.s.o. forEach ... C'est plus court à taper et la différence de performance à l'exécution n'est généralement pas importante dans mon expérience.

0voto

cestmoi Points 1

Voici mes deux centimes d'euros :

  • fonction résistible forpromise()
  • émule une boucle for classique
  • permet une sortie anticipée basée sur une logique interne, en renvoyant une valeur
  • peut collecter un tableau de résultats passés dans resolve/next/collect
  • La valeur par défaut est start=0, increment=1.
  • les exceptions lancées à l'intérieur de la boucle sont attrapées et passées à .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 votes

Ne créez pas de new Promise à l'intérieur d'un new Promise rappel. Il s'agit d'un cas amplifié de la anti-modèle de constructeur de promesses .

0 votes

De plus, je pensais que les promesses étaient destinées à éviter une L'enfer du rappel mais ici l'imbrication devient plus profonde avec chaque test...

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X