如何将Promise.all与对象一起用作输入

时间:2015-03-27 03:44:46

标签: javascript asynchronous promise

我一直在为自己使用的小型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函数将资源转换为promises对象。

问题在于它试图使用Promises.all来检查他们什么时候准备就绪,但是Promise.all只接受迭代作为输入 - 所以像我所拥有的对象是不可能的。 / p>

所以我尝试将对象转换为数组,这很好用,除了每个资源只是数组中的一个元素,并且没有一个键来识别它们。

这是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);
        });
    });
};

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

16 个答案:

答案 0 :(得分:9)

首先:废弃Promise构造函数,this usage is an antipattern


现在,针对您的实际问题:正确识别后,您缺少每个值的密钥。你需要在每个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;
    });
});

答案 1 :(得分:9)

如果您使用lodash库,则可以通过单行函数实现此目的:

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

答案 2 :(得分:3)

这是一个简单的ES2015函数,它接受一个具有可能promises属性的对象,并返回具有已解析属性的该对象的promise。

COURSE_NAME

用法:

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);
    });

}

请注意,@ Bergi的答案将返回一个新对象,而不是改变原始对象。如果您确实需要新对象,只需将传递给reduce函数的初始化值更改为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"}

答案 3 :(得分:3)

这是@Matt的答案,其中包含某些类型和重命名,并使用ECMA-2019 Object.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);
};

答案 4 :(得分:2)

使用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)));

答案 5 :(得分:2)

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

答案 6 :(得分:1)

根据此处接受的答案,我认为我提供的方法略有不同,似乎更容易理解:

&#13;
&#13;
// 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)
})
&#13;
&#13;
&#13;

用法:

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(),看看是否有人在写完这个答案后已经实现了这个,显然this npm package确实有一个实现,所以如果你喜欢这个小扩展,请使用它。

答案 7 :(得分:0)

编辑:这个问题似乎最近有所增加,所以我想我现在将这个问题添加到我现在用于几个项目的问题中。这比我两年前写的这个答案底部的代码更好很多

新的loadAll函数假设它的输入是将资产名称映射到promises的对象,它还使用了实验函数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);


所以我根据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);
            });
        });
    });
};

答案 8 :(得分:0)

缺少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);

答案 9 :(得分:0)

我编写了一个函数,该函数递归地等待对象中的promise,并将构造的对象返回给您。

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

答案 10 :(得分:0)

我推荐Sindre Sorhus的p-props。他的东西总是很棒。

答案 11 :(得分:0)

一种简单且最简单的方法是

Promise.all([yourObject]).then((result)=>{
  yourObject={...result}
}).catch((error)=>{console.log(error)})

答案 12 :(得分:0)

这样的事情怎么样?

const promiseObject = {
  foo: promise1,
  bar: promise2
}

await Promise.all(Object.values(object))

// ... do what you want with the awaited values.

答案 13 :(得分:0)

function resolveObject(obj) {
    return Promise.all(
      Object.entries(obj).map(async ([k, v]) => [k, await v])
    ).then(Object.fromEntries);
}

https://esdiscuss.org/topic/modify-promise-all-to-accept-an-object-as-a-parameter向该天才的Cyril Auburtin致谢

function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function test() {
    console.time(1);
    console.log(await resolveObject({
        foo: delay(5).then(()=>1),
        bar: delay(120).then(()=>2)
    }));
    console.timeEnd(1);
}

答案 14 :(得分:0)

创建函数:

const promiseAllFromObject = async promisesObject => (
    Object.keys(promisesObject).reduce(async (acc, key) => {
        const lastResult = await acc;
        return Object.assign(lastResult, { [key]: await promisesObject[key] });
    }, Promise.resolve({}))
);

用法:

promiseAllFromObject({
    abc: somePromise,
    xyz: someOtherPromise,
});

结果:

{
    abc: theResult,
    xyz: theOtherResult,
}

答案 15 :(得分:0)

需要这个,包括良好的 TypeScript 支持吗?

combine-promises 可以混合不同类型的对象值并推断出一个好的返回类型。

https://github.com/slorber/combine-promises

const result: { user: User; company: Company } = await combinePromises({
  user: fetchUser(),
  company: fetchCompany(),
});