私は自分の使用のために小さな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関数は、リソースをpromiseのオブジェクトに変換します。
問題は、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);
});
});
};
理想的には、何らかの方法で、各アイテムの識別子を保持しながら、リソースの約束のリストを適切に管理したいと思います。どんな助けでも感謝します、ありがとう。
lodash ライブラリを使用する場合、1行関数でこれを実現できます。
Promise.allValues = async (object) => {
return _.zipObject(_.keys(object), await Promise.all(_.values(object)))
}
まず第一に: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;
});
});
実際にそのためのライブラリを作成し、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'
// }
});
Promiseである可能性のあるプロパティを持つオブジェクトを受け取り、解決されたプロパティでそのオブジェクトのpromiseを返す簡単なES2015関数を次に示します。
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の答えは、元のオブジェクトを変更するのではなく、新しいオブジェクトを返すことに注意してください。新しいオブジェクトが必要な場合は、reduce関数に渡される初期化子の値を{}
に変更するだけです
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)));
ここで受け入れられた答えに基づいて、従うのが簡単だと思われる少し異なるアプローチを提供すると思いました:
_// 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)
})
_
使用法:
_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()
を調べて、この答えを書いた後に誰かがすでにこれを実装しているかどうかを確認し、明らかに このnpmパッケージ は実装そのため、この小さな拡張機能が気に入ったらそれを使用してください。
編集:この質問は最近少し牽引力を獲得しているようですので、私は今いくつかのプロジェクトで使用しているこの問題に私の現在の解決策を追加すると思いました。 lotは、2年前に書いたこの回答の最後のコードよりも優れています。
新しいloadAll関数は、その入力が資産名をプロミスにマッピングするオブジェクトであると想定し、すべての環境で使用できるわけではない実験関数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);
だから、ベルギの答えに基づいて適切なコードを思いついた。ここで、他の誰かが同じ問題を抱えているかどうかがわかります。
// 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);
});
});
});
};
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);
オブジェクト内の約束を再帰的に待機し、構築されたオブジェクトを返す関数を作成しました。
/**
* 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');
})();