84 votes

Tableau JavaScript .reduce avec async/await

Il semble que l'intégration de l'asynchronisme et de l'attente avec .reduce() pose problème, comme suit :

const data = await bodies.reduce(async(accum, current, index) => {
  const methodName = methods[index]
  const method = this[methodName]
  if (methodName == 'foo') {
    current.cover = await this.store(current.cover, id)
    console.log(current)
    return {
      ...accum,
      ...current
    }
  }
  return {
    ...accum,
    ...method(current.data)
  }
}, {})
console.log(data)

El data l'objet est enregistré avant le site this.store complète...

Je sais que vous pouvez utiliser Promise.all avec des boucles asynchrones, mais cela s'applique-t-il à .reduce() ?

159voto

Bergi Points 104242

Le problème est que les valeurs de votre accumulateur sont des promesses - ce sont des valeurs de retour de async function s. Pour obtenir une évaluation séquentielle (et que toutes les itérations sauf la dernière soient attendues), vous devez utiliser

const data = await array.reduce(async (accumP, current, index) => {
  const accum = await accumP;
  …
}, Promise.resolve(…));

Cela dit, pour async / await En général, je recommanderais à utiliser des boucles simples au lieu des méthodes d'itération des tableaux ils sont plus performants et souvent plus simples.

3 votes

Merci pour votre conseil à la fin. J'ai fini par utiliser une simple boucle for pour ce que je faisais, et c'était les mêmes lignes de code, mais beaucoup plus facile à lire...

3 votes

El initialValue de la reduce ne doit pas nécessairement être un Promise Toutefois, dans la plupart des cas, elle permet de clarifier l'intention.

0 votes

@EECOLOR Il devrait l'être, cependant. Je n'aime vraiment pas await devoir transformer une valeur simple en une promesse

6voto

Asaf Katz Points 2007

J'aime la réponse de Bergi, je pense que c'est la bonne voie à suivre.

J'aimerais également mentionner une de mes bibliothèques, appelée Awaity.js

Ce qui vous permet d'utiliser sans effort des fonctions comme reduce , map & filter con async / await :

import reduce from 'awaity/reduce';

const posts = await reduce([1,2,3], async (posts, id) => {

  const res = await fetch('/api/posts/' + id);
  const post = await res.json();

  return {
    ...posts,
    [id]: post
  };
}, {})

posts // { 1: { ... }, 2: { ... }, 3: { ... } }

0 votes

Chaque passage va-t-il être séquentiel ? Ou bien appelle-t-elle toutes ces fonctions await en un lot ?

1 votes

Séquentielle, puisque chaque itération dépend de la valeur de retour de la précédente.

2voto

Brandon K Points 603

Vous pouvez envelopper l'ensemble de vos blocs d'itérateurs map/reduce dans leur propre Promise.resolve et attendre qu'ils se terminent. Le problème, cependant, est que l'accumulateur ne contient pas les données/objets résultants que vous attendez à chaque itération. En raison de la chaîne interne async/await/Promise, l'accumulateur sera constitué de Promises réelles qui n'ont probablement pas encore été résolues malgré l'utilisation du mot-clé await avant votre appel au magasin (ce qui pourrait vous faire croire que l'itération ne reviendra pas tant que cet appel ne sera pas terminé et que l'accumulateur ne sera pas mis à jour).

Bien que ce ne soit pas la solution la plus élégante, une option que vous avez est de déplacer vos données hors du champ d'application et l'assigner en tant que variable d'objet. let afin que la liaison et la mutation appropriées puissent avoir lieu. Mettez ensuite à jour cet objet de données à partir de votre itérateur au fur et à mesure de la résolution des appels asynchrones, d'attente et de promesse.

/* allow the result object to be initialized outside of scope 
   rather than trying to spread results into your accumulator on iterations, 
   else your results will not be maintained as expected within the 
   internal async/await/Promise chain.
*/    
let data = {}; 

await Promise.resolve(bodies.reduce(async(accum, current, index) => {
  const methodName = methods[index]
  const method = this[methodName];
  if (methodName == 'foo') {
    // note: this extra Promise.resolve may not be entirely necessary
    const cover = await Promise.resolve(this.store(current.cover, id));
    current.cover = cover;
    console.log(current);
    data = {
      ...data,
      ...current,
    };
    return data;
  }
  data = {
    ...data,
    ...method(current.data)
  };
  return data;
}, {});
console.log(data);

1voto

ericP Points 648

[Ne pas aborder le problème exact du PO ; se concentrer sur les autres qui atterrissent ici].

La réduction est généralement utilisée lorsque vous avez besoin du résultat des étapes précédentes avant de pouvoir traiter la suivante. Dans ce cas, vous pouvez enchaîner les promesses :

promise = elts.reduce(
    async (promise, elt) => {
        return promise.then(async last => {
            return await f(last, elt)
        })
    }, Promise.resolve(0)) // or "" or [] or ...

Voici un exemple qui utilise fs.promise.mkdir() (bien sûr, il serait beaucoup plus simple d'utiliser mkdirSync, mais dans mon cas, c'est à travers un réseau) :

const Path = require('path')
const Fs = require('fs')

async function mkdirs (path) {
    return path.split(/\//).filter(d => !!d).reduce(
        async (promise, dir) => {
            return promise.then(async parent => {
                const ret = Path.join(parent, dir);
                try {
                    await Fs.promises.lstat(ret)
                } catch (e) {
                    console.log(`mkdir(${ret})`)
                    await Fs.promises.mkdir(ret)
                }
                return ret
            })
        }, Promise.resolve(""))
}

mkdirs('dir1/dir2/dir3')

Voici un autre exemple qui ajoute 100 + 200 ... 500 et attend un peu :

async function slowCounter () {
    const ret = await ([100, 200, 300, 400, 500]).reduce(
        async (promise, wait, idx) => {
            return promise.then(async last => {
                const ret = last + wait
                console.log(`${idx}: waiting ${wait}ms to return ${ret}`)
                await new Promise((res, rej) => setTimeout(res, wait))
                return ret
            })
        }, Promise.resolve(0))
    console.log(ret)
}

slowCounter ()

1voto

l30_4l3X Points 81

Parfois, la meilleure chose à faire est simplement de mettre les deux versions du code côte à côte, sync et async :

Version de la synchronisation :

const arr = [1, 2, 3, 4, 5];

const syncRev = arr.reduce((acc, i) => [i, ...acc], []); // [5, 4, 3, 2, 1] 

Asynchrone :

(async () => { 
   const asyncRev = await arr.reduce(async (promisedAcc, i) => {
      const id = await asyncIdentity(i); // could be id = i, just stubbing async op.
      const acc = await promisedAcc;
      return [id, ...acc];
   }, Promise.resolve([]));   // [5, 4, 3, 2, 1] 
})();

//async stuff
async function asyncIdentity(id) {
   return Promise.resolve(id);
}

const arr = [1, 2, 3, 4, 5];
(async () => {
    const asyncRev = await arr.reduce(async (promisedAcc, i) => {
        const id = await asyncIdentity(i);
        const acc = await promisedAcc;
        return [id, ...acc];
    }, Promise.resolve([]));

    console.log('asyncRev :>> ', asyncRev);
})();

const syncRev = arr.reduce((acc, i) => [i, ...acc], []);

console.log('syncRev :>> ', syncRev);

async function asyncIdentity(id) {
    return Promise.resolve(id);
}

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