Pregunta ¿Cómo accedo a los resultados prometidos anteriores en una cadena .then ()?


He reestructurado mi código para promesas, y construyó una maravillosa larga cadena de promesa plana, que consiste en múltiples .then() devoluciones de llamada. Al final quiero devolver algún valor compuesto, y necesito acceder a múltiples resultados promisorios intermedios. Sin embargo, los valores de resolución del centro de la secuencia no están dentro del alcance de la última devolución de llamada, ¿cómo puedo acceder a ellos?

function getExample() {
    return promiseA(…).then(function(resultA) {
        // Some processing
        return promiseB(…);
    }).then(function(resultB) {
        // More processing
        return // How do I gain access to resultA here?
    });
}

509
2018-01-31 10:41


origen


Respuestas:


Romper la cadena

Cuando necesite acceder a los valores intermedios de su cadena, debe dividir su cadena en las piezas individuales que necesita. En lugar de adjuntar una devolución de llamada y de alguna manera tratar de usar su parámetro varias veces, adjunte varias devoluciones de llamada a la misma promesa, donde sea que necesite el valor del resultado. No lo olvides, La promesa solo representa (proxies) un valor futuro! Luego de derivar una promesa del otro en una cadena lineal, use los prometedores combinadores que le proporciona su biblioteca para generar el valor del resultado.

Esto dará como resultado un flujo de control muy sencillo, una composición clara de funcionalidades y, por lo tanto, una fácil modularización.

function getExample() {
    var a = promiseA(…);
    var b = a.then(function(resultA) {
        // some processing
        return promiseB(…);
    });
    return Promise.all([a, b]).then(function([resultA, resultB]) {
        // more processing
        return // something using both resultA and resultB
    });
}

En lugar de desestructurar el parámetro en la devolución de llamada después Promise.all que solo estuvo disponible con ES6, en ES5 then la llamada sería reemplazada por un ingenioso método de ayuda proporcionado por muchas bibliotecas prometedoras (Q, Azulejo, cuando, ...): .spread(function(resultA, resultB) { ….

Bluebird también presenta un dedicado join función para reemplazar eso Promise.all+spread combinación con una construcción más simple (y más eficiente):

…
return Promise.join(a, b, function(resultA, resultB) { … });

306
2018-01-31 10:44



ECMAScript Harmony

Por supuesto, los diseñadores de idiomas también reconocieron este problema. Hicieron un montón de trabajo y el propuesta de funciones asincrónicas finalmente lo hizo en

ECMAScript 8

No necesitas un solo then ya sea de invocación o función de devolución de llamada, como en una función asincrónica (que devuelve una promesa al ser llamada), simplemente puede esperar a que las promesas se resuelvan directamente. También presenta estructuras de control arbitrarias como condiciones, bucles y cláusulas try-catch, pero por comodidad no las necesitamos aquí:

async function getExample() {
    var resultA = await promiseA(…);
    // some processing
    var resultB = await promiseB(…);
    // more processing
    return // something using both resultA and resultB
}

ECMAScript 6

Mientras esperábamos ES8, ya usamos un tipo de sintaxis similar. ES6 vino con funciones del generador, que permiten dividir la ejecución en pedazos en lugares arbitrariamente yield palabras clave. Esos segmentos se pueden ejecutar uno detrás del otro, independientemente, incluso de forma asíncrona, y eso es exactamente lo que hacemos cuando queremos esperar a una resolución prometedora antes de ejecutar el siguiente paso.

Hay bibliotecas dedicadas (como co o task.js), pero también muchas bibliotecas prometedoras tienen funciones de ayuda (Q, Azulejo, cuando, …) Esto hace esta asincrónica ejecución paso a paso para ti cuando les das una función de generador que rinde promesas.

var getExample = Promise.coroutine(function* () {
//               ^^^^^^^^^^^^^^^^^ Bluebird syntax
    var resultA = yield promiseA(…);
    // some processing
    var resultB = yield promiseB(…);
    // more processing
    return // something using both resultA and resultB
});

Esto funcionó en Node.js desde la versión 4.0, también algunos navegadores (o sus ediciones dev) sí soportaron la sintaxis del generador relativamente temprano.

ECMAScript 5

Sin embargo, si desea / necesita ser compatible con versiones anteriores, no puede usar aquellas sin un transpiler. Tanto las funciones del generador como las funciones asíncronas son compatibles con las herramientas actuales; consulte, por ejemplo, la documentación de Babel en generadores y funciones asincrónicas.

Y luego, también hay muchos otros compilar a JS languages que están dedicados a facilitar la programación asincrónica. Usualmente usan una sintaxis similar a await, (p.ej. Iced CoffeeScript), pero también hay otros que presentan un estilo Haskell do-notación (p. LatteJs, monádico, PureScript o LispyScript)


190
2018-01-31 10:43



Inspección síncrona

Asigna promesas para valores posteriores necesarios a variables y luego obtiene su valor a través de inspección síncrona. El ejemplo usa bluebird's .value() método, pero muchas bibliotecas proporcionan un método similar.

function getExample() {
    var a = promiseA(…);

    return a.then(function() {
        // some processing
        return promiseB(…);
    }).then(function(resultB) {
        // a is guaranteed to be fulfilled here so we can just retrieve its
        // value synchronously
        var aValue = a.value();
    });
}

Esto se puede usar para tantos valores como desee:

function getExample() {
    var a = promiseA(…);

    var b = a.then(function() {
        return promiseB(…)
    });

    var c = b.then(function() {
        return promiseC(…);
    });

    var d = c.then(function() {
        return promiseD(…);
    });

    return d.then(function() {
        return a.value() + b.value() + c.value() + d.value();
    });
}

86
2018-01-31 13:16



Anidamiento (y) cierres

El uso de cierres para mantener el alcance de las variables (en nuestro caso, los parámetros de la función de devolución de llamada correcta) es la solución natural de JavaScript. Con promesas, podemos arbitrariamente anidar y aplanar  .then() devoluciones de llamada: son semánticamente equivalentes, excepto por el alcance del interno.

function getExample() {
    return promiseA(…).then(function(resultA) {
        // some processing
        return promiseB(…).then(function(resultB) {
            // more processing
            return // something using both resultA and resultB;
        });
    });
}

Por supuesto, esto está construyendo una pirámide de indentación. Si la sangría es demasiado grande, aún puede aplicar las herramientas antiguas para contrarrestar el pirámide de la perdición: modularice, use funciones adicionales y aplana la cadena de promesas tan pronto como ya no necesite una variable.
En teoría, siempre puedes evitar más de dos niveles de anidamiento (al hacer que todos los cierres sean explícitos), en la práctica usa tantos como sea razonable.

function getExample() {
    // preprocessing
    return promiseA(…).then(makeAhandler(…));
}
function makeAhandler(…)
    return function(resultA) {
        // some processing
        return promiseB(…).then(makeBhandler(resultA, …));
    };
}
function makeBhandler(resultA, …) {
    return function(resultB) {
        // more processing
        return // anything that uses the variables in scope
    };
}

También puede usar funciones auxiliares para este tipo de aplicación parcial, me gusta _.partial de Guion bajo/lodash o el nativo .bind() método, para disminuir aún más la sangría:

function getExample() {
    // preprocessing
    return promiseA(…).then(handlerA);
}
function handlerA(resultA) {
    // some processing
    return promiseB(…).then(handlerB.bind(null, resultA));
}
function handlerB(resultA, resultB) {
    // more processing
    return // anything that uses resultA and resultB
}

50
2018-01-31 10:42



Paso explícito

Similar a anidar las devoluciones de llamada, esta técnica se basa en cierres. Sin embargo, la cadena se mantiene estable: en lugar de pasar solo el resultado más reciente, se pasa un objeto de estado por cada paso. Estos objetos de estado acumulan los resultados de las acciones anteriores, transmitiendo todos los valores que serán necesarios más tarde más el resultado de la tarea actual.

function getExample() {
    return promiseA(…).then(function(resultA) {
        // some processing
        return promiseB(…).then(b => [resultA, b]); // function(b) { return [resultA, b] }
    }).then(function([resultA, resultB]) {
        // more processing
        return // something using both resultA and resultB
    });
}

Aquí, esa pequeña flecha b => [resultA, b] es la función que se cierra resultA, y pasa una matriz de ambos resultados al siguiente paso. Que usa la sintaxis de desestructuración de parámetros para dividirlo nuevamente en variables únicas.

Antes de que la desestructuración estuviera disponible con ES6, se llamaba un ingenioso método de ayuda .spread() fue proporcionado por muchas bibliotecas prometedoras (Q, Azulejo, cuando, ...). Se necesita una función con múltiples parámetros, uno para cada elemento de la matriz, que se utilizará como .spread(function(resultA, resultB) { ….

Por supuesto, ese cierre necesario aquí puede simplificarse aún más mediante algunas funciones auxiliares, p.

function addTo(x) {
    // imagine complex `arguments` fiddling or anything that helps usability
    // but you get the idea with this simple one:
    return res => [x, res];
}

…
return promiseB(…).then(addTo(resultA));

Alternativamente, puede emplear Promise.all para producir la promesa de la matriz:

function getExample() {
    return promiseA(…).then(function(resultA) {
        // some processing
        return Promise.all([resultA, promiseB(…)]); // resultA will implicitly be wrapped
                                                    // as if passed to Promise.resolve()
    }).then(function([resultA, resultB]) {
        // more processing
        return // something using both resultA and resultB
    });
}

Y puede que no solo uses matrices, sino objetos arbitrariamente complejos. Por ejemplo, con _.extend o Object.assign en una función auxiliar diferente:

function augment(obj, name) {
    return function (res) { var r = Object.assign({}, obj); r[name] = res; return r; };
}

function getExample() {
    return promiseA(…).then(function(resultA) {
        // some processing
        return promiseB(…).then(augment({resultA}, "resultB"));
    }).then(function(obj) {
        // more processing
        return // something using both obj.resultA and obj.resultB
    });
}

Si bien este patrón garantiza una cadena plana y los objetos de estado explícito pueden mejorar la claridad, será tedioso para una cadena larga. Especialmente cuando necesita el estado solo esporádicamente, aún debe pasarlo a través de cada paso. Con esta interfaz fija, las devoluciones de llamada únicas en la cadena están bastante unidas e inflexibles para cambiar. Hace que el factorizar los pasos individuales sea más difícil, y las devoluciones de llamadas no se pueden suministrar directamente desde otros módulos; siempre necesitan estar envueltos en un código repetitivo que se preocupe por el estado. Las funciones auxiliares abstractas como las anteriores pueden aliviar un poco el dolor, pero siempre estarán presentes.


42
2018-01-31 10:42



Estado contextual mutable

La solución trivial (pero poco elegante y bastante erronea) es simplemente usar variables de mayor alcance (a las que todas las devoluciones de llamada en la cadena tienen acceso) y escribirles valores de resultado cuando las obtengas:

function getExample() {
    var resultA;
    return promiseA(…).then(function(_resultA) {
        resultA = _resultA;
        // some processing
        return promiseB(…);
    }).then(function(resultB) {
        // more processing
        return // something using both resultA and resultB
    });
}

En lugar de muchas variables, también se podría usar un objeto (inicialmente vacío), en el cual los resultados se almacenan como propiedades creadas dinámicamente.

Esta solución tiene varios inconvenientes:

  • El estado mutable es feoy las variables globales son malvadas.
  • Este patrón no funciona a través de los límites de las funciones, modularizar las funciones es más difícil ya que sus declaraciones no deben salir del alcance compartido
  • El alcance de las variables no impide acceder a ellas antes de que se inicialicen. Esto es especialmente probable para las construcciones de promesas complejas (bucles, ramificaciones, excpciones) donde las condiciones de carrera pueden ocurrir. Pasando el estado explícitamente, un diseño declarativo que promete alentar, obliga a un estilo de codificación más limpio que puede evitar esto.
  • Uno debe elegir el alcance para esas variables compartidas correctamente. Debe ser local a la función ejecutada para evitar condiciones de carrera entre múltiples invocaciones paralelas, como sería el caso si, por ejemplo, el estado se almacena en una instancia.

La biblioteca de Bluebird fomenta el uso de un objeto que se transfiere, utilizando su bind() método para asignar un objeto de contexto a una cadena de promesa. Será accesible desde cada función de devolución de llamada a través de lo inutilizable this palabra clave. Si bien las propiedades de los objetos son más propensas a los errores de escritura no detectados que las variables, el patrón es bastante ingenioso:

function getExample() {
    return promiseA(…)
    .bind({}) // Bluebird only!
    .then(function(resultA) {
        this.resultA = resultA;
        // some processing
        return promiseB(…);
    }).then(function(resultB) {
        // more processing
        return // something using both this.resultA and resultB
    }).bind(); // don't forget to unbind the object if you don't want the
               // caller to access it
}

Este enfoque se puede simular fácilmente en las bibliotecas de promesa que no son compatibles con .bind (aunque de una manera algo más detallada y no se puede usar en una expresión):

function getExample() {
    var ctx = {};
    return promiseA(…)
    .then(function(resultA) {
        this.resultA = resultA;
        // some processing
        return promiseB(…);
    }.bind(ctx)).then(function(resultB) {
        // more processing
        return // something using both this.resultA and resultB
    }.bind(ctx));
}

28
2018-01-31 10:43



El nodo 7.4 ahora admite llamadas asincrónicas / esperadas con la bandera de armonía.

Prueba esto:

async function getExample(){

  let response = await returnPromise();

  let response2 = await returnPromise2();

  console.log(response, response2)

}

getExample()

y ejecuta el archivo con:

node --harmony-async-await getExample.js

¡Simple como puede ser!


5
2018-01-21 22:14



Otra respuesta, usando babel-node versión <6

Utilizando async - await 

npm install -g babel@5.6.14

example.js:

async function getExample(){

  let response = await returnPromise();

  let response2 = await returnPromise2();

  console.log(response, response2)

}

getExample()

Entonces corre babel-node example.js ¡y voilá!


4
2017-11-20 19:59