¿Cómo devuelve correctamente varios valores de una promesa?

86

Recientemente me encontré con una situación determinada un par de veces, que no sabía cómo resolver correctamente. Suponga el siguiente código:

somethingAsync()
  .then( afterSomething )
  .then( afterSomethingElse )

function afterSomething( amazingData ) {
  return processAsync( amazingData );
}
function afterSomethingElse( processedData ) {
}

Ahora puede surgir una situación donde me gustaría tener acceso a amazingDataen afterSomethingElse.

Una solución obvia sería devolver una matriz o un hash de afterSomething, porque, bueno, solo puede devolver un valor de una función. Pero me pregunto si hay forma de afterSomethingElseaceptar 2 parámetros e invocarlos de la misma manera, ya que parece mucho más fácil de documentar y comprender.

Solo me pregunto acerca de esta posibilidad ya que la hay Q.spread, que hace algo similar a lo que quiero.

Der Hochstapler
fuente
La desestructuración de Assignment en ES6 ayudaría. verifique aquí
Ravi Teja

Respuestas:

88

No puede resolver una promesa con varias propiedades del mismo modo que no puede devolver varios valores de una función . Una promesa representa conceptualmente un valor a lo largo del tiempo, por lo que si bien puede representar valores compuestos, no puede poner varios valores en una promesa.

Una promesa se resuelve de forma inherente con un valor único: esto es parte de cómo funciona Q, cómo funciona la especificación Promises / A + y cómo funciona la abstracción .

Lo más cercano que puede obtener es usar Q.spready devolver matrices o usar la desestructuración de ES6 si es compatible o si está dispuesto a usar una herramienta de transpilación como BabelJS.

En cuanto a transmitir el contexto a través de una cadena de promesas, consulte el excelente canónico de Bergi al respecto .

Benjamin Gruenbaum
fuente
16
¿Qué tiene de malo resolver con un objeto que tiene múltiples propiedades? Parece una forma sencilla de sacar varios valores de una resolución.
jfriend00
6
Está perfectamente bien hacer eso
Benjamin Gruenbaum
También puede extender Promise para que se .spread()muestre como Bluebird en esta respuesta relacionada: stackoverflow.com/a/22776850/1624862
Kevin Ghadyani
El comportamiento de Promise.all () parece contradecir esto. Promise.all([a, b, c]).then(function(x, y, z) {...})funciona correctamente en todos los motores Javascript modernos con x, yyz evaluando los valores resueltos de a, by c. Por lo tanto, es más exacto decir que el lenguaje no le permite hacerlo fácilmente (o con cordura) desde el código de usuario (porque puede devolver una Promesa directamente desde una cláusula then, puede envolver sus valores en promesas y luego envolverlas con Promise .all () para obtener el comportamiento deseado, aunque de una manera complicada).
Austin Hemmelgarn
3
@AustinHemmelgarn Eso es simplemente falso, se Promise.allcumple con una matriz . En Promise.all([a,b]).then((a, b) => bes undefined. Es por eso que debes hacer lo .then(([a, b]) =>que es una tarea de desestructuración
Benjamin Gruenbaum
38

solo puede pasar un valor, pero puede ser una matriz con múltiples valores dentro, como ejemplo:

function step1(){
  let server = "myserver.com";
  let data = "so much data, very impresive";
  return Promise.resolve([server, data]);
}

Por otro lado, se puede utilizar la desestructuración de expresión para ES2015 para obtener los valores individuales.

function step2([server, data]){
  console.log(server); // print "myserver.com"
  console.log(data);   // print "so much data, very impresive"
  return Promise.resolve("done");
}

para llamar a ambos promesa, encadenándolos:

step1()
.then(step2)
.then((msg)=>{
  console.log(msg); // print "done"
})
Alejandro Silva
fuente
5
Se llama desestructuración , no "deconstructor", y no es un operador: - /
Bergi
3
Por cierto, puede usar la desestructuración directamente en los parámetros: function step2([server, data]) { …- de esa manera también evita asignar a globales implícitos. Y realmente debería usar returno Promise.resolve, no el new Promiseconstructor en sus ejemplos.
Bergi
¡Gracias @Bergi por las recomendaciones!
Alejandro Silva
19

Puede devolver un objeto que contenga ambos valores; no hay nada de malo en eso.

Otra estrategia es mantener el valor, mediante cierres, en lugar de pasarlo por:

somethingAsync().then(afterSomething);

function afterSomething(amazingData) {
  return processAsync(amazingData).then(function (processedData) {
    // both amazingData and processedData are in scope here
  });
}

Forma completa en lugar de parcialmente en línea (equivalente, posiblemente más consistente):

somethingAsync().then(function (amazingData) {
  return processAsync(amazingData).then(function (processedData) {
    // both amazingData and processedData are in scope here
  });
}
Kevin Reid
fuente
3
¿Está bien devolver un theninterior a otro then? ¿No es un anti-patrón ?
robe007
como dijo @ robe007, ¿no sería esto similar al 'infierno de devolución de llamada'? aquí, su anidación luego bloquea en lugar de funciones de devolución de llamada, esto anularía el propósito de tener promesas
Dheeraj
5

Dos cosas que puedes hacer, devolver un objeto

somethingAsync()
    .then( afterSomething )
    .then( afterSomethingElse );

function processAsync (amazingData) {
     //processSomething
     return {
         amazingData: amazingData, 
         processedData: processedData
     };
}

function afterSomething( amazingData ) {
    return processAsync( amazingData );
}

function afterSomethingElse( dataObj ) {
    let amazingData = dataObj.amazingData,
        processedData = dataObj.proccessedData;
}

¡Usa el alcance!

var amazingData;
somethingAsync()
  .then( afterSomething )
  .then( afterSomethingElse )

function afterSomething( returnedAmazingData ) {
  amazingData = returnedAmazingData;
  return processAsync( amazingData );
}
function afterSomethingElse( processedData ) {
  //use amazingData here
}
jemiloii
fuente
3

Así es como creo que debería estar haciendo.

dividiendo la cadena

Debido a que ambas funciones usarán amazingData , tiene sentido tenerlas en una función dedicada. Por lo general, hago eso cada vez que quiero reutilizar algunos datos, por lo que siempre está presente como una función arg.

Como su ejemplo está ejecutando algún código, supongo que todo está declarado dentro de una función. Lo llamaré toto () . Luego tendremos otra función que se ejecutará tanto afterSomething () como afterSomethingElse () .

function toto() {
    return somethingAsync()
        .then( tata );
}

También notará que agregué una declaración de devolución , ya que generalmente es el camino a seguir con Promesas: siempre devuelve una promesa para que podamos seguir encadenando si es necesario. Aquí, somethingAsync () producirá datos asombrosos y estará disponible en todas partes dentro de la nueva función.

Ahora bien, ¿cómo se verá esta nueva función normalmente depende de si processAsync () también es asincrónico ?

processAsync no asincrónico

No hay razón para complicar demasiado las cosas si processAsync () no es asincrónico. Algún código secuencial bueno y antiguo lo haría.

function tata( amazingData ) {
    var processed = afterSomething( amazingData );
    return afterSomethingElse( amazingData, processed );
}

function afterSomething( amazingData ) {
    return processAsync( amazingData );
}
function afterSomethingElse( amazingData, processedData ) {
}

Tenga en cuenta que no importa si afterSomethingElse () está haciendo algo asincrónico o no. Si lo hace, se devolverá una promesa y la cadena podrá continuar. Si no es así, se devolverá el valor del resultado. Pero debido a que la función se llama desde un then () , el valor se incluirá en una promesa de todos modos (al menos en Javascript sin formato).

processAsync asincrónico

Si processAsync () es asincrónico, el código se verá ligeramente diferente. Aquí consideramos que afterSomething () y afterSomethingElse () no se reutilizarán en ningún otro lugar.

function tata( amazingData ) {
    return afterSomething()
        .then( afterSomethingElse );

    function afterSomething( /* no args */ ) {
        return processAsync( amazingData );
    }
    function afterSomethingElse( processedData ) {
        /* amazingData can be accessed here */
    }
}

Lo mismo que antes para afterSomethingElse () . Puede ser asincrónico o no. Se devolverá una promesa o un valor envuelto en una promesa resuelta.


Su estilo de codificación es bastante parecido al que solía hacer, por eso respondí incluso después de 2 años. No soy un gran fanático de tener funciones anónimas en todas partes. Me cuesta leer. Incluso si es bastante común en la comunidad. Es como reemplazamos el infierno de devolución de llamada por un purgatorio de promesas .

También me gusta mantener el nombre de las funciones en el entonces corto. De todos modos, solo se definirán localmente. Y la mayoría de las veces llamarán a otra función definida en otro lugar, tan reutilizable, para hacer el trabajo. Incluso hago eso para funciones con solo 1 parámetro, por lo que no necesito que la función entre y salga cuando agrego / elimino un parámetro a la firma de la función.

Ejemplo de comer

Aquí hay un ejemplo:

function goingThroughTheEatingProcess(plenty, of, args, to, match, real, life) {
    return iAmAsync()
        .then(chew)
        .then(swallow);

        function chew(result) {
            return carefullyChewThis(plenty, of, args, "water", "piece of tooth", result);
        }

        function swallow(wine) {
            return nowIsTimeToSwallow(match, real, life, wine);
        }
}

function iAmAsync() {
    return Promise.resolve("mooooore");
}

function carefullyChewThis(plenty, of, args, and, some, more) {
    return true;
}

function nowIsTimeToSwallow(match, real, life, bobool) {
}

No se centre demasiado en Promise.resolve () . Es solo una forma rápida de crear una promesa resuelta. Lo que trato de lograr con esto es tener todo el código que estoy ejecutando en una sola ubicación, justo debajo de los thens . Todas las demás funciones con un nombre más descriptivo son reutilizables.

El inconveniente de esta técnica es que define muchas funciones. Pero me temo que es un dolor necesario para evitar tener funciones anónimas por todas partes. ¿Y cuál es el riesgo de todos modos: un desbordamiento de pila? (¡broma!)


El uso de matrices u objetos como se define en otras respuestas también funcionaría. Esta de alguna manera es la respuesta propuesta por Kevin Reid .

También puede utilizar bind () o Promise.all () . Tenga en cuenta que aún requerirán que divida su código.

usando bind

Si desea mantener sus funciones reutilizables pero realmente no necesita mantener lo que está dentro del entonces muy corto, puede usar bind () .

function tata( amazingData ) {
    return afterSomething( amazingData )
        .then( afterSomethingElse.bind(null, amazingData) );
}

function afterSomething( amazingData ) {
    return processAsync( amazingData );
}
function afterSomethingElse( amazingData, processedData ) {
}

Para mantenerlo simple, bind () antepondrá la lista de argumentos (excepto el primero) a la función cuando se llame.

usando Promise.all

En su publicación mencionó el uso de spread () . Nunca usé el marco que está usando, pero así es como debería poder usarlo.

Algunos creen que Promise.all () es la solución a todos los problemas, así que supongo que merece ser mencionado.

function tata( amazingData ) {
    return Promise.all( [ amazingData, afterSomething( amazingData ) ] )
        .then( afterSomethingElse );
}

function afterSomething( amazingData ) {
    return processAsync( amazingData );
}
function afterSomethingElse( args ) {
    var amazingData = args[0];
    var processedData = args[1];
}

Puede pasar datos a Promise.all () - tenga en cuenta la presencia de la matriz - siempre que las promesas, pero asegúrese de que ninguna de las promesas falle; de ​​lo contrario, dejará de procesarse.

Y en lugar de definir nuevas variables a partir del argumento args , debería poder usar spread () en lugar de then () para todo tipo de trabajo increíble.

Gabriel
fuente
3

Simplemente crea un objeto y extrae argumentos de ese objeto.

let checkIfNumbersAddToTen = function (a, b) {
return new Promise(function (resolve, reject) {
 let c = parseInt(a)+parseInt(b);
 let promiseResolution = {
     c:c,
     d : c+c,
     x : 'RandomString'
 };
 if(c===10){
     resolve(promiseResolution);
 }else {
     reject('Not 10');
 }
});
};

Extraiga argumentos de promiseResolution.

checkIfNumbersAddToTen(5,5).then(function (arguments) {
console.log('c:'+arguments.c);
console.log('d:'+arguments.d);
console.log('x:'+arguments.x);
},function (failure) {
console.log(failure);
});

fuente
2

Todo lo que devuelva de una promesa se incluirá en una promesa que se desenvolverá en la siguiente .then()etapa.

Se vuelve interesante cuando necesita devolver una o más promesas junto con uno o más valores sincrónicos como;

Promise.resolve([Promise.resolve(1), Promise.resolve(2), 3, 4])
       .then(([p1,p2,n1,n2]) => /* p1 and p2 are still promises */);

En estos casos, sería esencial utilizar Promise.all()para obtener p1y p2promesas desenvueltas en la siguiente .then()etapa, como

Promise.resolve(Promise.all([Promise.resolve(1), Promise.resolve(2), 3, 4]))
       .then(([p1,p2,n1,n2]) => /* p1 is 1, p2 is 2, n1 is 3 and n2 is 4 */);
Redu
fuente
1

Puede marcar Observable representado por Rxjs , le permite devolver más de un valor.

codelovesme
fuente
0

Simplemente devuelva una tupla:

async add(dto: TDto): Promise<TDto> {
console.log(`${this.storeName}.add(${dto})`);
return firebase.firestore().collection(this.dtoName)
  .withConverter<TDto>(this.converter)
  .add(dto)
  .then(d => [d.update(this.id, d.id), d.id] as [any, string])
  .then(x => this.get(x[1]));

}

swissmawi
fuente