web-dev-qa-db-ja.com

オブジェクトを入力として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関数は、リソースを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);
        });
    });
};

理想的には、何らかの方法で、各アイテムの識別子を保持しながら、リソースの約束のリストを適切に管理したいと思います。どんな助けでも感謝します、ありがとう。

21
Matt

lodash ライブラリを使用する場合、1行関数でこれを実現できます。

Promise.allValues = async (object) => {
  return _.zipObject(_.keys(object), await Promise.all(_.values(object)))
}
11
david.sevcik

まず第一に: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;
    });
});
10
Bergi

実際にそのためのライブラリを作成し、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'
  // }
});
3
Marcelo Waisman

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関数に渡される初期化子の値を{}に変更するだけです

3
Zak Henry

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)));
2
Congelli501

ここで受け入れられた答えに基づいて、従うのが簡単だと思われる少し異なるアプローチを提供すると思いました:

_// 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パッケージ は実装そのため、この小さな拡張機能が気に入ったらそれを使用してください。

1
Patrick Roberts

編集:この質問は最近少し牽引力を獲得しているようですので、私は今いくつかのプロジェクトで使用しているこの問題に私の現在の解決策を追加すると思いました。 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);
            });
        });
    });
};
0
Matt

Promise.obj()メソッドがありません

バニラJavaScript、ライブラリ、ループ、突然変異のない短いソリューション

最新の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);
0
rsp

オブジェクト内の約束を再帰的に待機し、構築されたオブジェクトを返す関数を作成しました。

/**
* 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');
})();
    
0