Как использовать Promise.all с объектом в качестве входного
Я работаю над небольшой 2D-библиотекой игр для собственного использования, и у меня возникла проблема. В библиотеке называется loadGame, которая принимает информацию о зависимостях в качестве входных данных (файлы ресурсов и список скриптов, которые должны быть выполнены). Вот пример.
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"
]
})
Каждый элемент в ресурсах имеет ключ, который используется игрой для доступа к этому конкретному ресурсу. Функция loadGame преобразует ресурсы в объект promises.
Проблема заключается в том, что он пытается использовать promises.all для проверки, когда все они готовы, но Promise.all принимает только итерации в качестве входов - поэтому объект, подобный тому, что у меня есть, не может быть и речи.
Итак, я попытался преобразовать объект в массив, это отлично работает, за исключением того, что каждый ресурс является всего лишь элементом в массиве и не имеет ключа для идентификации.
Здесь код для 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);
});
});
};
В идеале я хотел бы каким-то образом правильно управлять списком promises для ресурсов, сохраняя при этом идентификатор для каждого элемента. Любая помощь будет оценена, спасибо.
Ответы
Ответ 1
Прежде всего: удалите этот конструктор Promise
, это использование антипаттерна!
Теперь к вашей реальной проблеме: как вы правильно определили, вы упускаете ключ для каждого значения. Вам нужно будет передать его в каждом обещании, чтобы вы могли восстановить объект после ожидания всех элементов:
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;
});
Более мощные библиотеки, такие как Bluebird, также предоставляют эту функцию в качестве вспомогательной функции, например, Promise.props
.
Кроме того, вы не должны использовать эту псевдорекурсивную функцию load
. Вы можете просто связать обещания вместе:
….then(function (resources) {
return game.scripts.reduce(function(queue, script) {
return queue.then(function() {
return getScript(root + script);
});
}, Promise.resolve()).then(function() {
return resources;
});
});
Ответ 2
Если вы используете библиотеку lodash, вы можете добиться этого с помощью функции, состоящей из одной строки:
Promise.allValues = async (object) => {
return _.zipObject(_.keys(object), await Promise.all(_.values(object)))
}
Ответ 3
Вот простая функция ES2015, которая принимает объект со свойствами, которые могут быть promises, и возвращает обещание этого объекта с разрешенными свойствами.
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);
});
}
Использование:
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"}
Обратите внимание, что ответ @Bergi возвращает новый объект, а не мутирует исходный объект. Если вы хотите новый объект, просто измените значение инициализатора, которое передается в функцию уменьшения, на {}
Ответ 4
Я действительно создал библиотеку только для этого и опубликовал ее в github и npm:
https://github.com/marcelowa/promise-all-properties
https://www.npmjs.com/package/promise-all-properties
Единственное, что вам нужно будет назначить имя свойства для каждого обещания в объекте...
вот пример из README
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'
// }
});
Ответ 5
Использование async/wait и 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)));
Ответ 6
Редактировать: этот вопрос, кажется, набирает обороты в последнее время, поэтому я решил добавить свое текущее решение этой проблемы, которое я сейчас использую в нескольких проектах. Это намного лучше, чем код в нижней части этого ответа, который я написал два года назад.
Новая функция loadAll предполагает, что ее входные данные являются объектами, сопоставляющими имена активов с обещаниями, а также использует экспериментальную функцию Object.entries, которая может быть недоступна во всех средах.
// fromEntries :: [[a, b]] -> {a: b}
// Does the reverse of Object.entries.
const fromEntries = 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(fromEntries);
Ответ 7
Основываясь на принятом ответе здесь, я подумал, что предлагаю немного другой подход, который выглядит проще:
// 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)
})
Ответ 8
Отсутствует метод Promise.obj()
Более короткое решение с ванильным JavaScript, без библиотек, без циклов, без мутаций
Вот более короткое решение, чем другие ответы, с использованием современного синтаксиса JavaScript.
Средняя линия process = ...
является рекурсивной и обрабатывает глубокие объекты.
Это создает отсутствующий метод Promise.obj()
, который работает как Promise.all()
, но для объектов:
const asArray = obj => [].concat(...Object.entries(obj));
const process = ([key, val, ...rest], aggregated = {}) =>
rest.length ?
process(rest, {...aggregated, [key]: val}) :
{...aggregated, [key]: val};
const promisedAttributes = obj => Promise.all(asArray(obj)).then(process);
// Promise.obj = promisedAttributes;
Лучше не использовать последнюю строку! Гораздо лучшая идея состоит в том, что вы экспортируете этот promisedAttributes
в качестве вспомогательной функции, которую вы повторно используете.
Ответ 9
Я написал функцию, которая рекурсивно ожидает обещания внутри объекта и возвращает вам созданный объект.
/**
* function for mimicking async action
*/
function load(value) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(value);
}, Math.random() * 1000);
});
}
/**
* Recursively iterates over object properties and awaits all promises.
*/
async function fetch(obj) {
if (obj instanceof Promise) {
obj = await obj;
return fetch(obj);
} else if (Array.isArray(obj)) {
return await Promise.all(obj.map((item) => fetch(item)));
} else if (obj.constructor === Object) {
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
obj[key] = await fetch(obj[key]);
}
return obj;
} else {
return obj;
}
}
// now lets load a world object which consists of a bunch of promises nested in each other
let worldPromise = {
level: load('world-01'),
startingPoint: {
x: load('0'),
y: load('0'),
},
checkpoints: [
{
x: load('10'),
y: load('20'),
}
],
achievments: load([
load('achievement 1'),
load('achievement 2'),
load('achievement 3'),
]),
mainCharacter: {
name: "Artas",
gear: {
helmet: load({
material: load('steel'),
level: load(10),
}),
chestplate: load({
material: load('steel'),
level: load(20),
}),
boots: load({
material: load('steel'),
level: load(20),
buff: load('speed'),
}),
}
}
};
//this will result an object like this
/*
{
level: Promise { <pending> },
startingPoint: {
x: Promise { <pending> },
y: Promise { <pending> }
},
checkpoints: [ { x: [Promise], y: [Promise] } ],
achievments: Promise { <pending> },
mainCharacter: {
name: 'Artas',
gear: {
helmet: [Promise],
chestplate: [Promise],
boots: [Promise]
}
}
}
*/
//Now by calling fetch function, all promise values will be populated
//And you can see that computation time is ~1000ms which means that all processes are being computed in parallel.
(async () => {
console.time('start');
console.log(worldPromise);
let world = await fetch(worldPromise);
console.log(world);
console.timeEnd('start');
})();