javascript - example - Cómo utilizar Promise.all con un objeto como entrada
promise.all typescript (5)
Antes que nada: ¡chatarra ese constructor de Promise
, este uso es un antipatrón !
Ahora, a su problema real: como ha identificado correctamente, le falta la clave para cada valor. Tendrá que pasarlo dentro de cada promesa, para que pueda reconstruir el objeto después de haber esperado todos los elementos:
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;
});
Bibliotecas más poderosas como Bluebird también proporcionarán esto como una función de ayuda, como Promise.props
.
Además, no deberías usar esa función de load
pseudo-recursiva. Simplemente puede encadenar promesas juntas:
….then(function (resources) {
return game.scripts.reduce(function(queue, script) {
return queue.then(function() {
return getScript(root + script);
});
}, Promise.resolve()).then(function() {
return resources;
});
});
He estado trabajando en una pequeña biblioteca de juegos 2D para mi propio uso, y me he encontrado con un pequeño problema. Hay una función particular en la biblioteca llamada loadGame que toma información de dependencia como entrada (se pueden ejecutar archivos de recursos y una lista de scripts). Aquí hay un ejemplo.
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"
]
})
Cada elemento de los recursos tiene una clave que usa el juego para acceder a ese recurso en particular. La función loadGame convierte los recursos en un objeto de promesas.
El problema es que trata de utilizar Promises.all para verificar cuando estén listos, pero Promise.all acepta solo los iterables como entradas, por lo que un objeto como el que tengo está fuera de discusión.
Así que traté de convertir el objeto en una matriz, esto funciona muy bien, excepto que cada recurso es solo un elemento en una matriz y no tiene una clave para identificarlos.
Aquí está el código para 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);
});
});
};
Idealmente, me gustaría de alguna manera administrar adecuadamente una lista de promesas de recursos sin dejar de mantener un identificador para cada elemento. Cualquier ayuda será apreciada, gracias.
Aquí hay una función ES2015 simple que toma un objeto con propiedades que podrían ser promesas y devuelve una promesa de ese objeto con propiedades resueltas.
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);
});
}
Uso:
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"}
Tenga en cuenta que la respuesta de @Bergi devolverá un nuevo objeto, no mutará el objeto original. Si desea un objeto nuevo, simplemente cambie el valor del inicializador que se pasa a la función de reducción a {}
Basado en la respuesta aceptada aquí, pensé que ofrecería un enfoque ligeramente diferente que parece más fácil de seguir:
// 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)
})
Uso:
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)
}
Busqué Promise.allKeys()
para ver si alguien ya había implementado esto después de escribir esta respuesta, y aparentemente este paquete npm tiene una implementación para él, así que úselo si le gusta esta pequeña extensión.
Editar: Esta pregunta parece estar ganando un poco de tracción últimamente, así que pensé en agregar mi solución actual a este problema que estoy usando en un par de proyectos ahora. Es mucho mejor que el código al final de esta respuesta que escribí hace dos años.
La nueva función loadAll asume que su entrada es un objeto que asigna nombres de activos a promesas, y también hace uso de la función experimental Object.entries, que puede no estar disponible en todos los entornos.
// 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);
Así que he encontrado el código correcto basado en la respuesta de Bergi. Aquí está si alguien más está teniendo el mismo problema.
// 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);
});
});
});
};
Usando async / await y 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)));