Pytanie Jak korzystać z Promise.all z obiektem jako wejściem


Pracowałem nad małą biblioteką gier 2D na własny użytek i napotkałem na mały problem. W bibliotece znajduje się określona funkcja o nazwie loadGame, która pobiera dane o zależnościach jako dane wejściowe (pliki zasobów i listę skryptów do wykonania). Oto przykład.

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"
    ]
})

Każda pozycja w zasobach ma klucz używany przez grę do uzyskania dostępu do danego zasobu. Funkcja loadGame przekształca zasoby w obiekt obietnic.

Problem polega na tym, że próbuje użyć Promises.all, aby sprawdzić, kiedy wszystko jest gotowe, ale Promise.all akceptuje tylko iterables jako dane wejściowe - więc obiekt taki jak to, co mam, nie wchodzi w rachubę.

Tak więc próbowałem przekonwertować obiekt na tablicę, działa to świetnie, z tym że każdy zasób jest tylko elementem tablicy i nie ma klucza do ich identyfikacji.

Oto kod dla 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);
        });
    });
};

Idealnie chciałbym w jakiś sposób prawidłowo zarządzać listą obietnic dla zasobów, wciąż zachowując identyfikator każdego elementu. Każda pomoc będzie doceniona, dzięki.


16
2018-03-27 03:44


pochodzenie


Dlaczego nie zwrócisz tablicy tablic? values, gdzie poszczególne tablice mają również odpowiednie klawisze? - thefourtheye
Lubię to? [['myKey', promiseForResource], ['otherKey', promiseForResource]] - Matt
Tak. To jeden ze sposobów rozwiązania tego problemu. - thefourtheye
W porządku, ale potem Promise.all nie sprawdziłby tego poprawnie, prawda? - Matt


Odpowiedzi:


Po pierwsze: Złomuj to Promise konstruktor, to użycie jest antipattern!


Teraz, do twojego aktualnego problemu: Jak poprawnie zidentyfikowałeś, brakuje ci klucza dla każdej wartości. Będziesz musiał go przekazać w każdej obietnicy, abyś mógł zrekonstruować obiekt po oczekiwaniu na wszystkie przedmioty:

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

Większe biblioteki, takie jak Bluebird, również zapewnią to jako funkcję pomocniczą, na przykład Promise.props.


Ponadto nie powinieneś używać tego pseudo-rekursywnego load funkcjonować. Możesz po prostu połączyć obietnice razem:

….then(function (resources) {
    return game.scripts.reduce(function(queue, script) {
        return queue.then(function() {
            return getScript(root + script);
        });
    }, Promise.resolve()).then(function() {
        return resources;
    });
});

6
2018-03-27 05:13



Czy masz więcej zasobów na temat anty-wzór? Jestem bardzo nowy w obietnicach i mam problemy z owinięciem głowy wokół tego, jak działa. Thx dla kodu skryptowego btw, który wygląda o wiele lepiej. - Matt
Hm, myślałem, że powiązane pytanie i jego odpowiedzi są dość skomplikowane. Czego brakuje? - Bergi
Obietnice ES6 wymagają pewnej iteracji (przynajmniej w teorii), którą można wykorzystać - ta odpowiedź jest dobra z punktu widzenia nauczania :) - Benjamin Gruenbaum
@ Daniel: Tak, w funkcjach niskiego poziomu, które nie mogą korzystać z innych obiecujących funkcji, używałbyś wtedy new Promise. Chociaż często wystarczy, nawet nie musisz, jak są funkcje pomocnicze aby uniknąć kodu standardowego. - Bergi
Ach ok, chyba kod getPromiseFor() jest bardzo proste do wyprowadzenia z tego pytania. - Roamer-1888


Oto prosta funkcja ES2015, która pobiera obiekt z właściwościami, które mogą być obietnicami i zwraca obietnicę tego obiektu o rozwiązanych właściwościach.

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

}

Stosowanie:

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"}

Zauważ, że odpowiedź @ Bergi zwróci nowy obiekt, a nie zmutuje oryginalny obiekt. Jeśli chcesz nowy obiekt, po prostu zmień wartość inicjalizatora, która jest przekazywana do funkcji redukcji {}


2
2018-06-18 15:07





Używanie async / await i 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
2018-06-06 15:20



Dziękuję bardzo @ Congelli501, to po prostu działa! Stworzyłem jsFiddle z Twoim rozwiązaniem, aby lepiej go zrozumieć i zobaczyć w akcji: jsfiddle.net/natterstefan/69yjkm2p. - natterstefan


Opierając się na zaakceptowanej tutaj odpowiedzi, pomyślałem, że zaproponuję nieco inne podejście, które wydaje się łatwiejsze do wykonania:

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

Stosowanie:

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

Podniosłem wzrok Promise.allKeys() aby sprawdzić, czy ktoś już to zaimplementował po napisaniu tej odpowiedzi i najwyraźniej ten pakiet npm ma implementację, więc użyj tego, jeśli podoba ci się to małe rozszerzenie.


1
2017-07-18 21:38



Nie powinno się tego nazywać allValues zamiast tego lub nawet lepiej Promise.all akceptować parametry obiektu i zachowywać się w ten sposób? - Augustin Riedinger
@AugustinRiedinger pewnie, konwencja nazewnictwa może być dowolna, ponieważ i tak nie jest to standard. Co do twojej drugiej sugestii, mógłbym to zrobić, ale chciałem tego uniknąć, ponieważ ludzie mogą błędnie sądzić, że to standardowa implementacja Promise.all(). Tak długo, jak to rozumiesz, możesz włączyć tę funkcję do swojego kodu, jak tylko chcesz. - Patrick Roberts
Pewnie. Ciekawe, że mamy inne podejście. Kiedy używam defineProperty lub prototypeoznacza to, że widzę kod jako polyfill dla jakiejś funkcji, którą chciałbym mieć zamiast jakiegoś niestandardowego kodu osobistego. - Augustin Riedinger
@AugustinRiedinger osobiście, gdy poproszony o rozwiązanie problemu, który wydaje się być powszechnie wielokrotnego użytku, nie jestem przeciwny modyfikowaniu prototypejeśli jest zrozumiałe, że kod nie jest częścią jakiegoś opublikowanego lub redystrybuowanego projektu, którego będą używać inne osoby. - Patrick Roberts


Jeśli użyjesz lać biblioteka, możesz to osiągnąć za pomocą funkcji jednej linijki:

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

1
2018-05-20 17:10





Edycja: To pytanie wydaje się ostatnio zyskiwać trochę na znaczeniu, więc pomyślałem, że dodam obecne rozwiązanie tego problemu, którego używam teraz w kilku projektach. To jest los lepiej niż kod znajdujący się u dołu tej odpowiedzi, którą napisałem dwa lata temu.

Nowa funkcja loadAll zakłada, że ​​jej dane wejściowe to odwzorowanie nazw zasobów na obietnice, a także korzysta z eksperymentalnej funkcji Object.entries, która może nie być dostępna we wszystkich środowiskach.

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


Tak więc wymyśliłem poprawny kod oparty na odpowiedzi Bergiego. Tutaj jest, jeśli ktoś inny ma ten sam problem.

// 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
2018-03-28 04:09





Brakujący Promise.obj() metoda

Krótszy roztwór z wanilią JavaScript, bez bibliotek, bez pętli, bez mutacji

Oto krótsze rozwiązanie niż inne odpowiedzi, przy użyciu nowoczesnej składni JavaScript.

To tworzy brakujące Promise.obj() metoda, która działa jak Promise.all() ale w przypadku obiektów:

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

Zauważ, że powyższe zmienia globalne Promise obiekt, więc lepiej zmienić ostatni wiersz na:

const objAll = obj => Promise.all(a(obj)).then(o);

0
2017-07-10 22:06





Właściwie to stworzyłem bibliotekę i opublikowałem ją na github i npm:

https://github.com/marcelowa/promise-all-properties
https://www.npmjs.com/package/promise-all-properties

Jedyne, co musisz zrobić, to przypisać nazwę właściwości każdej obietnicy w obiekcie ... oto przykład z 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'
  // }
});

0
2017-08-07 08:30