Cómo manejar múltiples esperas en la función asíncrona

8

Tengo que realizar varias llamadas a la API que se obtienen a través de una API, escriben datos en la base de datos a través de la API, envían resultados al front-end a través de otra API.

He escrito la función asincrónica con esperar como a continuación:

Los primeros 2 deben ejecutarse uno tras otro, pero el tercero puede ejecutarse independientemente y no es necesario esperar a que se completen las primeras 2 declaraciones de captación.

let getToken= await fetch(url_for_getToken);
let getTokenData = await getToken.json();

let writeToDB = await fetch(url_for_writeToDB);
let writeToDBData = await writeToDB.json();

let frontEnd = await fetch(url_for_frontEnd);
let frontEndData = await frontEnd.json();

¿Cuál es la mejor manera de manejar tales declaraciones de búsqueda múltiple?

Yasar Abdullah
fuente
66
Podrías echar un vistazo a Promise.all .
Yannick K
2
@YannickK ¿Promise.all sería necesario aquí? ¿No podría simplemente usar .then () en su lugar? No está esperando la finalización de ambos, sino el primero, luego el segundo, luego el tercero, independientemente de esos dos.
Kobe
1
@Kobe Creo que el problema principal en este caso es que OP quiere separar las llamadas del servidor y del cliente, ya que no son dependientes entre sí, y sería tonto en cuanto al rendimiento si se esperaran el uno al otro, pero si alguno de ellos fallan quieres un rechazo. Definitivamente tiene razón en que podría prescindir de él Promise.all, pero en este caso me imagino que sería más limpio (y más fácil de construir en el futuro) si completara todo en una Promise.allllamada, específicamente para el manejo de errores.
Yannick K
@Kobe Porque Promise.alles esencial para el manejo adecuado de errores y esperar a que se cumplan las promesas primero, luego segundo y tercero.
Bergi
1
La respuesta más simple resuelve mejor el problema, pero desafortunadamente fue rechazado inmerecidamente. Vale la pena intentarlo, @Yasar Abdulllah.
AndreasPizsa

Respuestas:

2

Hay muchas formas, pero la más universal es ajustar cada ruta de código asíncrono en una función asíncrona. Esto le brinda flexibilidad de mezclar y combinar valores de retorno asíncronos como desee. En su ejemplo, incluso puede codificar en línea con async iife's:

await Promise.all([
  (async() => {
    let getToken = await fetch(url_for_getToken);
    let getTokenData = await getToken.json();

    let writeToDB = await fetch(url_for_writeToDB);
    let writeToDBData = await writeToDB.json();
  })(),
  (async() => {
    let frontEnd = await fetch(url_for_frontEnd);
    let frontEndData = await frontEnd.json();
  })()
]);
marzelin
fuente
Finalmente, una respuesta adecuada! : D
Bergi
1

Puede usar .then(), en lugar de esperar:

fetch(url_for_getToken)
  .then(getToken => getToken.json())
  .then(async getTokenData => {
    let writeToDB = await fetch(url_for_writeToDB);
    let writeToDBData = await writeToDB.json();
    // Carry on
  })

fetch(url_for_frontEnd)
  .then(frontEnd => frontEnd.json())
  .then(frontEndData => {
    // Carry on  
  })
Kobe
fuente
1
No se olvide de manejar los errores en esas cadenas de promesa, o de Promise.allellos, y devolverlos a la persona que llama.
Bergi
1

Es más fácil si trabaja con "creadores" de promesas (= función que devuelve promesas) en lugar de promesas en bruto. Primero, defina:

const fetchJson = (url, opts) => () => fetch(url, opts).then(r => r.json())

que devuelve tal "creador". Ahora, aquí hay dos utilidades para el encadenamiento en serie y paralelo, que aceptan tanto promesas en bruto como "creadores":

const call = f => typeof f === 'function' ? f() : f;

const parallel = (...fns)  => Promise.all(fns.map(call));

async function series(...fns) {
    let res = [];

    for (let f of fns)
        res.push(await call(f));

    return res;
}

Entonces, el código principal se puede escribir así:

let [[getTokenData, writeToDBData], frontEndData] = await parallel(
    series(
        fetchJson(url_for_getToken),
        fetchJson(url_for_writeToDB),
    ),
    fetchJson(url_for_frontEnd),
)

Si no le gusta el contenedor "creador" dedicado, puede definir fetchJsonnormalmente

const fetchJson = (url, opts) => fetch(url, opts).then(r => r.json())

y use continuaciones en línea justo donde serieso parallelse llaman:

let [[getTokenData, writeToDBData], frontEndData] = await parallel(
    series(
        () => fetchJson('getToken'),
        () => fetchJson('writeToDB'),
    ),
    () => fetchJson('frontEnd'), // continuation not necessary, but looks nicer
)

Para llevar la idea más allá, podemos hacer seriesy paralleldevolver "creadores", así como promesas. De esta manera, podemos construir "circuitos" anidados arbitrarios de promesas en serie y paralelas y obtener los resultados en orden. Ejemplo de trabajo completo:

const call = f => typeof f === 'function' ? f() : f;

const parallel = (...fns)  => () => Promise.all(fns.map(call));

const series = (...fns) => async () => {
    let res = [];

    for (let f of fns)
        res.push(await call(f));

    return res;
};

//

const request = (x, time) => () => new Promise(resolve => {
    console.log('start', x);
    setTimeout(() => {
        console.log('end', x)
        resolve(x)
    }, time)
});

async function main() {
    let chain = series(
        parallel(
            series(
                request('A1', 500),
                request('A2', 200),
            ),
            series(
                request('B1', 900),
                request('B2', 400),
                request('B3', 400),
            ),
        ),
        parallel(
            request('C1', 800),
            series(
                request('C2', 100),
                request('C3', 100),
            )
        ),
    );

    let results = await chain();

    console.log(JSON.stringify(results))
}

main()
.as-console-wrapper { max-height: 100% !important; top: 0; }

georg
fuente
¿Por qué no devuelves la promesa fetchJson? No puedo ver ningún beneficio al envolver una promesa con otra función. Y la utilidad de serieses cuestionable ya que la siguiente función de una serie generalmente requiere el valor de retorno de la (s) función (es) anterior (es).
marzelin
@marzelin: las promesas comienzan inmediatamente después de la creación, por series(promiseA, promiseB)lo tanto , comenzarán Ay Bal mismo tiempo.
georg
1
Ah, sí. Toda esta ceremonia solo para usar series. Prefiero preferir async iife para secuencias y en Promise.allSettledlugar de parallel.
marzelin
@marzelin: encuentro este limpiador de sintaxis (ver actualización).
georg
1
@ KamilKiełczewski: He actualizado la publicación ... La idea es que esta sintaxis de continuación permite construir "flujos" de promesa anidados sin mucha molestia.
georg
-2

Ejecute una solicitud independiente ( writeToDB) al principio y sinawait

let writeToDB = fetch(url_for_writeToDB);

let getToken = await fetch(url_for_getToken);
let getTokenData = await getToken.json();

// ...

Kamil Kiełczewski
fuente
1
@Bergi, la pregunta no es sobre el manejo de errores: el manejo de errores (como try-catch) es importante pero es un tema separado
Kamil Kiełczewski
awaittiene incorporado el manejo de errores: cuando la promesa se rechaza, se obtiene una excepción, la promesa devuelta por la async functionllamada se rechazará y la persona que llama se dará cuenta. Si sugiere eliminar la awaitpalabra clave y ejecutar la cadena de promesa de forma independiente, no debe hacerlo sin pensar en el manejo de errores. Una respuesta que al menos no lo menciona (o conserva el comportamiento original de la persona que llama que recibe un rechazo) es una mala respuesta.
Bergi
1
Esta respuesta resuelve el problema de OP y también es la más simple de todas las respuestas presentadas hasta ahora. Tengo curiosidad por saber por qué esto fue rechazado. El OP declaró que " el tercero puede ejecutarse de forma independiente y no es necesario esperar a que se completen las primeras 2 declaraciones de recuperación ", por lo que ejecutarlo primero está bien.
AndreasPizsa
@AndreasPizsa Es muy simple . No lo habría rechazado si hubiera al menos un descargo de responsabilidad de que los errores no se manejan, tan pronto como se menciona, cualquiera puede opinar sobre si es apropiado o no para su caso, incluso podría ser para el OP . Pero ignorar esto conduce a los peores errores de todos y hace que esta respuesta no sea útil.
Bergi
1
Gracias por comentar @Bergi. Dado que la pregunta de OP tampoco incluía el manejo de errores, supongo que se deja fuera por brevedad y más allá del alcance de la pregunta. Pero sí, cualquiera puede votar con su propia opinión.
AndreasPizsa