我一直在为自己使用的小型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只接受迭代作为输入 - 所以像我所拥有的对象是不可能的。 / p>
所以我尝试将对象转换为数组,这很好用,除了每个资源只是数组中的一个元素,并且没有一个键来识别它们。
这是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);
});
});
};
理想情况下,我希望以某种方式正确管理资源的承诺列表,同时仍然保留每个项目的标识符。任何帮助将不胜感激,谢谢。
答案 0 :(得分:9)
首先:废弃Promise
构造函数,this usage is an antipattern!
现在,针对您的实际问题:正确识别后,您缺少每个值的密钥。你需要在每个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;
});
});
答案 1 :(得分:9)
如果您使用lodash库,则可以通过单行函数实现此目的:
Promise.allValues = async (object) => {
return _.zipObject(_.keys(object), await Promise.all(_.values(object)))
}
答案 2 :(得分:3)
这是一个简单的ES2015函数,它接受一个具有可能promises属性的对象,并返回具有已解析属性的该对象的promise。
COURSE_NAME
用法:
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);
});
}
请注意,@ Bergi的答案将返回一个新对象,而不是改变原始对象。如果您确实需要新对象,只需将传递给reduce函数的初始化值更改为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"}
答案 3 :(得分:3)
这是@Matt的答案,其中包含某些类型和重命名,并使用ECMA-2019 Object.fromEntries
。
// delayName :: (k, Promise a) -> Promise (k, a)
const delayName = ([name, promise]) => promise.then((result) => [name, result]);
export type PromiseValues<TO> = {
[TK in keyof TO]: Promise<TO[TK]>;
};
// promiseObjectAll :: {k: Promise a} -> Promise {k: a}
export const promiseObjectAll = <T>(object: PromiseValues<T>): Promise<T> => {
const promiseList = Object.entries(object).map(delayName);
return Promise.all(promiseList).then(Object.fromEntries);
};
答案 4 :(得分:2)
使用async / await和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)));
答案 5 :(得分:2)
我实际上为此创建了一个库,并将其发布到github和npm:
https://github.com/marcelowa/promise-all-properties
https://www.npmjs.com/package/promise-all-properties
唯一的事情是您将需要为对象中的每个promise分配一个属性名称... 这是自述文件中的一个示例
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'
// }
});
答案 6 :(得分:1)
根据此处接受的答案,我认为我提供的方法略有不同,似乎更容易理解:
// 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)
})
&#13;
用法:
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)
}
我查了Promise.allKeys()
,看看是否有人在写完这个答案后已经实现了这个,显然this npm package确实有一个实现,所以如果你喜欢这个小扩展,请使用它。
答案 7 :(得分:0)
编辑:这个问题似乎最近有所增加,所以我想我现在将这个问题添加到我现在用于几个项目的问题中。这比我两年前写的这个答案底部的代码更好很多。
新的loadAll函数假设它的输入是将资产名称映射到promises的对象,它还使用了实验函数Object.entries,它可能并非在所有环境中都可用。
// 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);
所以我根据Bergi的答案提出了正确的代码。如果其他人遇到同样的问题,那就是这样。
// 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);
});
});
});
};
答案 8 :(得分:0)
Promise.obj()
方法使用现代JavaScript语法,这是比其他答案更短的解决方案。
这会创建缺少的Promise.obj()
方法,其作用类似于Promise.all()
,但适用于对象:
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);
请注意,以上更改了全局Promise
对象,因此最好将最后一行更改为:
const objAll = obj => Promise.all(a(obj)).then(o);
答案 9 :(得分:0)
我编写了一个函数,该函数递归地等待对象中的promise,并将构造的对象返回给您。
/**
* 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');
})();
答案 10 :(得分:0)
我推荐Sindre Sorhus的p-props。他的东西总是很棒。
答案 11 :(得分:0)
一种简单且最简单的方法是
Promise.all([yourObject]).then((result)=>{
yourObject={...result}
}).catch((error)=>{console.log(error)})
答案 12 :(得分:0)
这样的事情怎么样?
const promiseObject = {
foo: promise1,
bar: promise2
}
await Promise.all(Object.values(object))
// ... do what you want with the awaited values.
答案 13 :(得分:0)
function resolveObject(obj) {
return Promise.all(
Object.entries(obj).map(async ([k, v]) => [k, await v])
).then(Object.fromEntries);
}
在https://esdiscuss.org/topic/modify-promise-all-to-accept-an-object-as-a-parameter向该天才的Cyril Auburtin致谢
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function test() {
console.time(1);
console.log(await resolveObject({
foo: delay(5).then(()=>1),
bar: delay(120).then(()=>2)
}));
console.timeEnd(1);
}
答案 14 :(得分:0)
创建函数:
const promiseAllFromObject = async promisesObject => (
Object.keys(promisesObject).reduce(async (acc, key) => {
const lastResult = await acc;
return Object.assign(lastResult, { [key]: await promisesObject[key] });
}, Promise.resolve({}))
);
用法:
promiseAllFromObject({
abc: somePromise,
xyz: someOtherPromise,
});
结果:
{
abc: theResult,
xyz: theOtherResult,
}
答案 15 :(得分:0)
需要这个,包括良好的 TypeScript 支持吗?
combine-promises
可以混合不同类型的对象值并推断出一个好的返回类型。
https://github.com/slorber/combine-promises
const result: { user: User; company: Company } = await combinePromises({
user: fetchUser(),
company: fetchCompany(),
});