可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I've been working on a small 2D game library for my own use, and I've run into a bit of a problem. There is a particular function in the library called loadGame that takes dependency info as input (resource files, and a list of scripts ot be executed). Here's an example.
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"
]
})
Each item in resources has a key that is used by the game to access that particular resource. The loadGame function converts the resources into an object of promises.
The problem is that it tries to use Promises.all to check for when they're all ready, but Promise.all accepts only iterables as inputs - so an object like what I have is out of the question.
So I tried to convert the object into an array, this works great, except each resource is just an element in an array and doesn't have a key to identify them.
Here's the code for 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);
});
});
};
Ideally I'd like for some way to properly manage a list of promises for resources while still mantaining an identifier for each item. Any help would be appreciated, thanks.
回答1:
First of all: Scrap that Promise
constructor, this usage is an antipattern!
Now, to your actual problem: As you have correctly identified, you are missing the key for each value. You will need to pass it inside each promise, so that you can reconstruct the object after having awaited all items:
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;
});
Mightier libraries such as Bluebird will also provide this as a helper function, like Promise.props
.
Also, you shouldn't use that pseudo-recursive load
function. You can simply chain promises together:
….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:
If you use lodash library, you can achieve this by a one-liner function:
Promise.allValues = async (object) => {
return _.zipObject(_.keys(object), await Promise.all(_.values(object)))
}
回答3:
Here is a simple ES2015 function that takes an object with properties that might be promises and returns a promise of that object with resolved properties.
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"}
Note that @Bergi's answer will return a new object, not mutate the original object. If you do want a new object, just change the initializer value that is passed into the reduce function to {}
回答4:
Using async/await and 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:
Based off the accepted answer here, I thought I'd offer a slightly different approach that seems easier to follow:
// 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)
}
I looked up Promise.allKeys()
to see if someone had already implemented this after writing this answer, and apparently this npm package does have an implementation for it, so use that if you like this little extension.
回答6:
I actually created a library just for that and published it to github and npm:
https://github.com/marcelowa/promise-all-properties
https://www.npmjs.com/package/promise-all-properties
The only thing is that you will need to assign a property name for each promise in the object...
here's an example from the 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'
// }
});
回答7:
Edit: This question seems to be gaining a little traction lately, so I thought I'd add my current solution to this problem which I'm using in a couple projects now. It's a lot better than the code at the bottom of this answer which I wrote two years ago.
The new loadAll function assume its input is an object mapping asset names to promises, and it also makes use of the experimental function Object.entries, which may not be available in all environments.
// 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);
So I've come up with the proper code based on Bergi's answer. Here it is if anyone else is having the same problem.
// 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:
Missing Promise.obj()
method
A shorter solution with vanilla JavaScript, no libraries, no loops, no mutation
Here is a shorter solution than other answers, using modern JavaScript syntax.
This creates a missing Promise.obj()
method that works like Promise.all()
but for objects:
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);
Note that the above changes the global Promise
object so it's better to change the last line to:
const objAll = obj => Promise.all(a(obj)).then(o);