web-dev-qa-db-fra.com

Comment utiliser Promise.all avec un objet en entrée

J'ai travaillé sur une petite bibliothèque de jeux 2D pour mon propre usage et j'ai rencontré un problème. Il existe une fonction particulière dans la bibliothèque appelée loadGame qui prend en entrée les informations de dépendance (fichiers de ressources et liste de scripts à exécuter). Voici un exemple.

loadGame({
    "root" : "/source/folder/for/game/",

    "resources" : {
        "soundEffect" : "audio/sound.mp3",
        "someImage" : "images/something.png",
        "someJSON" : "json/map.json"
    },

    "scripts" : [
        "js/helperScript.js",
        "js/mainScript.js"
    ]
})

Chaque élément dans les ressources a une clé qui est utilisée par le jeu pour accéder à cette ressource particulière. La fonction loadGame convertit les ressources en un objet de promesses.

Le problème est qu’il essaie d’utiliser Promises.all pour vérifier quand ils sont tous prêts, mais Promise.all n’accepte que les itérables en tant qu’entrées - un objet comme celui que j’ai est hors de question.

J'ai donc essayé de convertir l'objet en tableau, cela fonctionne très bien, sauf que chaque ressource est simplement un élément d'un tableau et n'a pas de clé pour l'identifier.

Voici le code pour loadGame:

var loadGame = function (game) {
    return new Promise(function (fulfill, reject) {
        // the root folder for the game
        var root = game.root || '';

        // these are the types of files that can be loaded
        // getImage, getAudio, and getJSON are defined elsewhere in my code - they return promises
        var types = {
            jpg : getImage,
            png : getImage,
            bmp : getImage,

            mp3 : getAudio,
            ogg : getAudio,
            wav : getAudio,

            json : getJSON
        };

        // the object of promises is created using a mapObject function I made
        var resources = mapObject(game.resources, function (path) {
            // get file extension for the item
            var extension = path.match(/(?:\.([^.]+))?$/)[1];

            // find the correct 'getter' from types
            var get = types[extension];

            // get it if that particular getter exists, otherwise, fail
            return get ? get(root + path) :
                reject(Error('Unknown resource type "' + extension + '".'));
        });

        // load scripts when they're done
        // this is the problem here
        // my 'values' function converts the object into an array
        // but now they are nameless and can't be properly accessed anymore
        Promise.all(values(resources)).then(function (resources) {
            // sequentially load scripts
            // maybe someday I'll use a generator for this
            var load = function (i) {
                // load script
                getScript(root + game.scripts[i]).then(function () {
                    // load the next script if there is one
                    i++;

                    if (i < game.scripts.length) {
                        load(i);
                    } else {
                        // all done, fulfill the promise that loadGame returned
                        // this is giving an array back, but it should be returning an object full of resources
                        fulfill(resources);
                    }
                });
            };

            // load the first script
            load(0);
        });
    });
};

Idéalement, j'aimerais pouvoir gérer correctement une liste de promesses de ressources tout en conservant un identifiant pour chaque élément. Toute aide serait appréciée Merci.

20
Matt

Tout d’abord: Scrap this Promise constructeur, cet usage est un antipattern !


Maintenant, à votre problème actuel: Comme vous l'avez correctement identifié, il vous manque la clé pour chaque valeur. Vous devrez le passer à l'intérieur de chaque promesse pour pouvoir reconstruire l'objet après avoir attendu tous les éléments:

function mapObjectToArray(obj, cb) {
    var res = [];
    for (var key in obj)
        res.Push(cb(obj[key], key));
    return res;
}

return Promise.all(mapObjectToArray(input, function(arg, key) {
    return getPromiseFor(arg, key).then(function(value) {
         return {key: key, value: value};
    });
}).then(function(arr) {
    var obj = {};
    for (var i=0; i<arr.length; i++)
        obj[arr[i].key] = arr[i].value;
    return obj;
});

Des bibliothèques plus puissantes telles que Bluebird fourniront également cette fonction, telle que Promise.props .


De plus, vous ne devriez pas utiliser cette fonction pseudo-récursive load. Vous pouvez simplement enchaîner les promesses ensemble:

….then(function (resources) {
    return game.scripts.reduce(function(queue, script) {
        return queue.then(function() {
            return getScript(root + script);
        });
    }, Promise.resolve()).then(function() {
        return resources;
    });
});
9
Bergi

Si vous utilisez lodash library, vous pouvez y parvenir grâce à une fonction à une ligne:

Promise.allValues = async (object) => {
  return _.zipObject(_.keys(object), await Promise.all(_.values(object)))
}
7
david.sevcik

Voici une fonction ES2015 simple qui prend un objet avec des propriétés qui pourraient être des promesses et renvoie une promesse de cet objet avec des propriétés résolues.

function promisedProperties(object) {

  let promisedProperties = [];
  const objectKeys = Object.keys(object);

  objectKeys.forEach((key) => promisedProperties.Push(object[key]));

  return Promise.all(promisedProperties)
    .then((resolvedValues) => {
      return resolvedValues.reduce((resolvedObject, property, index) => {
        resolvedObject[objectKeys[index]] = property;
        return resolvedObject;
      }, object);
    });

}

Usage:

promisedProperties({a:1, b:Promise.resolve(2)}).then(r => console.log(r))
//logs Object {a: 1, b: 2}

class User {
  constructor() {
    this.name = 'James Holden';
    this.ship = Promise.resolve('Rocinante');
  }
}

promisedProperties(new User).then(r => console.log(r))
//logs User {name: "James Holden", ship: "Rocinante"}

Notez que la réponse de @ Bergi renverra un nouvel objet et non de muter l'objet d'origine. Si vous voulez un nouvel objet, il suffit de changer la valeur d'initialiseur transmise à la fonction de réduction par {}

3
Zak Henry

Utiliser async/wait et lodash:

// If resources are filenames
const loadedResources = _.zipObject(_.keys(resources), await Promise.all(_.map(resources, filename => {
    return promiseFs.readFile(BASE_DIR + '/' + filename);
})))

// If resources are promises
const loadedResources = _.zipObject(_.keys(resources), await Promise.all(_.values(resources)));
2
Congelli501

En fait, j'ai créé une bibliothèque juste pour ça et je l'ai publiée dans github et npm:

https://github.com/marcelowa/promise-all-properties
https://www.npmjs.com/package/promise-all-properties

La seule chose à faire est que vous devrez attribuer un nom de propriété à chaque promesse de l’objet.

import promiseAllProperties from 'promise-all-properties';

const promisesObject = {
  someProperty: Promise.resolve('resolve value'),
  anotherProperty: Promise.resolve('another resolved value'),
};

const promise = promiseAllProperties(promisesObject);

promise.then((resolvedObject) => {
  console.log(resolvedObject);
  // {
  //   someProperty: 'resolve value',
  //   anotherProperty: 'another resolved value'
  // }
});
1
Marcelo Waisman

Sur la base de la réponse acceptée ici, je pensais proposer une approche légèrement différente qui semble plus facile à suivre:

// Promise.all() for objects
Object.defineProperty(Promise, 'allKeys', {
  configurable: true,
  writable: true,
  value: async function allKeys(object) {
    const resolved = {}
    const promises = Object
      .entries(object)
      .map(async ([key, promise]) =>
        resolved[key] = await promise
      )

    await Promise.all(promises)

    return resolved
  }
})

// usage
Promise.allKeys({
  a: Promise.resolve(1),
  b: 2,
  c: Promise.resolve({})
}).then(results => {
  console.log(results)
})

Promise.allKeys({
  bad: Promise.reject('bad error'),
  good: 'good result'
}).then(results => {
  console.log('never invoked')
}).catch(error => {
  console.log(error)
})

Usage:

try {
  const obj = await Promise.allKeys({
    users: models.User.find({ rep: { $gt: 100 } }).limit(100).exec(),
    restrictions: models.Rule.find({ passingRep: true }).exec()
  })

  console.log(obj.restrictions.length)
} catch (error) {
  console.log(error)
}

J'ai regardé Promise.allKeys() pour voir si quelqu'un l'avait déjà implémenté après avoir écrit cette réponse, et apparemment ce paquet npm a une implémentation, alors utilisez-le si vous aimez cette petite extension.

1
Patrick Roberts

Edit: Cette question semble gagner un peu en popularité ces derniers temps. J'ai donc pensé ajouter ma solution actuelle à ce problème, que j'utilise actuellement dans quelques projets. C'est un lot meilleur que le code au bas de cette réponse que j'ai écrite il y a deux ans.

La nouvelle fonction loadAll suppose que son entrée est un objet mappant des noms d'actif sur des promesses et utilise également la fonction expérimentale Object.entries, qui peut ne pas être disponible dans tous les environnements.

// unentries :: [(a, b)] -> {a: b}
const unentries = list => {
    const result = {};

    for (let [key, value] of list) {
        result[key] = value;
    }

    return result;
};

// addAsset :: (k, Promise a) -> Promise (k, a)
const addAsset = ([name, assetPromise]) =>
    assetPromise.then(asset => [name, asset]);

// loadAll :: {k: Promise a} -> Promise {k: a}
const loadAll = assets =>
    Promise.all(Object.entries(assets).map(addAsset)).then(unentries);


J'ai donc créé le code approprié basé sur la réponse de Bergi. La voici si quelqu'un d'autre a le même problème.

// maps an object and returns an array
var mapObjectToArray = function (obj, action) {
    var res = [];

    for (var key in obj) res.Push(action(obj[key], key));

    return res;
};

// converts arrays back to objects
var backToObject = function (array) {
    var object = {};

    for (var i = 0; i < array.length; i ++) {
        object[array[i].name] = array[i].val;
    }

    return object;
};

// the actual load function
var load = function (game) {
    return new Promise(function (fulfill, reject) {
        var root = game.root || '';

        // get resources
        var types = {
            jpg : getImage,
            png : getImage,
            bmp : getImage,

            mp3 : getAudio,
            ogg : getAudio,
            wav : getAudio,

            json : getJSON
        };

        // wait for all resources to load
        Promise.all(mapObjectToArray(game.resources, function (path, name) {
            // get file extension
            var extension = path.match(/(?:\.([^.]+))?$/)[1];

            // find the getter
            var get = types[extension];

            // reject if there wasn't one
            if (!get) return reject(Error('Unknown resource type "' + extension + '".'));

            // get it and convert to 'object-able'
            return get(root + path, name).then(function (resource) {
                return {val : resource, name : name};
            });

            // someday I'll be able to do this
            // return get(root + path, name).then(resource => ({val : resource, name : name}));
        })).then(function (resources) {
            // convert resources to object
            resources = backToObject(resources);

            // attach resources to window
            window.resources = resources;

            // sequentially load scripts
            return game.scripts.reduce(function (queue, path) {
                return queue.then(function () {
                    return getScript(root + path);
                });
            }, Promise.resolve()).then(function () {
                // resources is final value of the whole promise
                fulfill(resources);
            });
        });
    });
};
0
Matt

Méthode Promise.obj() manquante

Une solution plus courte avec JavaScript vanille, pas de bibliothèques, pas de boucles, pas de mutation

Voici une solution plus courte que les autres réponses, utilisant la syntaxe JavaScript moderne.

Cela crée une méthode Promise.obj() manquante qui fonctionne comme Promise.all() mais pour les objets:

const a = o => [].concat(...Object.entries(o));
const o = ([x, y, ...r], a = {}) => r.length ? o(r, {...a, [x]: y}) : {...a, [x]: y};
Promise.obj = obj => Promise.all(a(obj)).then(o);

Notez que ce qui précède modifie l'objet global Promise. Il est donc préférable de modifier la dernière ligne en:

const objAll = obj => Promise.all(a(obj)).then(o);
0
rsp