如何使用 Promise.all 将对象作为输入

IT技术 javascript asynchronous promise
2021-03-11 06:55:51

我一直在开发一个供我自己使用的小型 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);
        });
    });
};

理想情况下,我想要某种方式来正确管理资源的Promise列表,同时仍然保留每个项目的标识符。任何帮助将不胜感激,谢谢。

6个回答

如果你使用lodash库,你可以通过一个单行函数来实现:

Promise.allValues = async (object) => {
  return _.zipObject(_.keys(object), await Promise.all(_.values(object)))
}
请不要修改内置对象,如Promise.
2021-04-23 06:55:51

首先:废弃那个Promise构造函数,这种用法是一种反模式


现在,对于您的实际问题:正如您正确识别的那样,您缺少每个值的键。您需要在每个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函数。您可以简单地将Promise链接在一起:

….then(function (resources) {
    return game.scripts.reduce(function(queue, script) {
        return queue.then(function() {
            return getScript(root + script);
        });
    }, Promise.resolve()).then(function() {
        return resources;
    });
});
你有更多关于反模式的资源吗?我对Promise很陌生,而且我很难理解它的工作方式。顺便说一下,脚本链接代码的 Thx 看起来好多了。
2021-04-23 06:55:51
嗯,我认为链接的问题及其答案非常详细。缺什么?
2021-05-01 06:55:51
ES6 Promise采用可以使用的可迭代(至少在理论上) - 不过从教学的角度来看,这个答案很好:)
2021-05-03 06:55:51
好吧,我想getPromiseFor()从问题中得出的代码非常简单。
2021-05-05 06:55:51
@DanielB:是的,在不能使用其他Promise返回函数的低级函数中,您将使用new Promise. 尽管通常情况下您实际上甚至不必这样做,因为有帮助函数可以避免样板代码。
2021-05-15 06:55:51

我实际上为此创建了一个库并将其发布到 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'
  // }
});

这是一个简单的 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);
    });

}

用法:

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)));
非常感谢@Congelli501,它只是有效!我已经用您的解决方案创建了一个 jsFiddle,以便更好地理解它并查看它的实际效果:jsfiddle.net/natterstefan/69yjkm2p
2021-04-17 06:55:51