Javascript 如何使用 Promise.all 将对象作为输入
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/29292921/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
How to use Promise.all with an object as input
提问by Matt
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.
我一直在开发一个供我自己使用的小型 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"
]
})
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.
资源中的每个项目都有一个密钥,游戏使用该密钥来访问该特定资源。loadGame 函数将资源转换为 promise 对象。
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.
问题是它试图使用 Promises.all 来检查它们何时都准备好了,但是 Promise.all 只接受可迭代对象作为输入——所以像我这样的对象是不可能的。
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:
这是 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.
理想情况下,我想要某种方式来正确管理资源的承诺列表,同时仍然保留每个项目的标识符。任何帮助将不胜感激,谢谢。
回答by david.sevcik
回答by Bergi
First of all: Scrap that Promiseconstructor, this usage is an antipattern!
首先:废弃那个Promise构造函数,这种用法是一种反模式!
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.
更强大的库,如 Bluebird 也将提供它作为辅助函数,如Promise.props.
Also, you shouldn't use that pseudo-recursive loadfunction. You can simply chain promises together:
此外,您不应使用该伪递归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;
});
});
回答by Marcelo Waisman
I actually created a library just for that and published it to github and npm:
我实际上为此创建了一个库并将其发布到 github 和 npm:
https://github.com/marcelowa/promise-all-properties
https://www.npmjs.com/package/promise-all-properties
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'
// }
});
回答by Zak Henry
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.
这是一个简单的 ES2015 函数,它接受一个具有可能是 Promise 的属性的对象,并返回该对象的具有已解析属性的 Promise。
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 {}
请注意,@Bergi 的回答将返回一个新对象,而不是改变原始对象。如果您确实想要一个新对象,只需将传递给reduce 函数的初始化值更改为{}
回答by Congelli501
Using async/await and lodash:
使用 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)));
回答by Patrick Roberts
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 packagedoes have an implementation for it, so use that if you like this little extension.
Promise.allKeys()在写完这个答案后,我抬头看看是否有人已经实现了这个,显然这个 npm 包确实有一个实现,所以如果你喜欢这个小扩展,请使用它。
回答by Matt
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 lotbetter 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.
新的 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);
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.
所以我根据 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);
});
});
});
};
回答by rsp
Missing Promise.obj()method
缺少Promise.obj()方法
A shorter solution with vanilla JavaScript, no libraries, no loops, no mutation
使用 vanilla JavaScript 的更短的解决方案,没有库,没有循环,没有突变
Here is a shorter solution than other answers, using modern JavaScript syntax.
这是一个比其他答案更短的解决方案,使用现代 JavaScript 语法。
The middle line process = ...is recursive and handles deep objects.
中间线process = ...是递归的,处理深层对象。
This creates a missing Promise.obj()method that works like Promise.all()but for objects:
这会创建一个缺失的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;
Better not use the last line! A much better idea is that you export this promisedAttributesas a utility function that you reuse.
最好不要使用最后一行!一个更好的主意是将其导出promisedAttributes为可重用的实用程序函数。
回答by Mathieu CAROFF
Here is @Matt's answer, with some types and some renames, and using ECMA-2019Object.fromEntries.
这是@Matt 的答案,有一些类型和一些重命名,并使用ECMA-2019Object.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);
};

