Подтвердить что ты не робот

Как использовать 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 преобразует ресурсы в объект promises.

Проблема заключается в том, что он пытается использовать 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);
        });
    });
};

В идеале я хотел бы каким-то образом правильно управлять списком promises для ресурсов, сохраняя при этом идентификатор для каждого элемента. Любая помощь будет оценена, спасибо.

4b9b3361

Ответ 1

Прежде всего: удалите этот конструктор 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;
    });
});

Ответ 2

Если вы используете библиотеку lodash, вы можете добиться этого с помощью функции, состоящей из одной строки:

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

Ответ 3

Вот простая функция ES2015, которая принимает объект со свойствами, которые могут быть promises, и возвращает обещание этого объекта с разрешенными свойствами.

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 возвращает новый объект, а не мутирует исходный объект. Если вы хотите новый объект, просто измените значение инициализатора, которое передается в функцию уменьшения, на {}

Ответ 4

Я действительно создал библиотеку только для этого и опубликовал ее в 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'
  // }
});

Ответ 5

Использование async/wait и 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)));

Ответ 6

Редактировать: этот вопрос, кажется, набирает обороты в последнее время, поэтому я решил добавить свое текущее решение этой проблемы, которое я сейчас использую в нескольких проектах. Это намного лучше, чем код в нижней части этого ответа, который я написал два года назад.

Новая функция 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);

Ответ 7

Основываясь на принятом ответе здесь, я подумал, что предлагаю немного другой подход, который выглядит проще:

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

Ответ 8

Отсутствует метод Promise.obj()

Более короткое решение с ванильным JavaScript, без библиотек, без циклов, без мутаций

Вот более короткое решение, чем другие ответы, с использованием современного синтаксиса JavaScript.

Средняя линия process = ... является рекурсивной и обрабатывает глубокие объекты.

Это создает отсутствующий метод 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;

Лучше не использовать последнюю строку! Гораздо лучшая идея состоит в том, что вы экспортируете этот promisedAttributes в качестве вспомогательной функции, которую вы повторно используете.

Ответ 9

Я написал функцию, которая рекурсивно ожидает обещания внутри объекта и возвращает вам созданный объект.

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