Diferencia entre "promesa de devolución en espera" y "promesa de devolución"
Dados los ejemplos de código siguientes, ¿existe alguna diferencia en el comportamiento y, de ser así, cuáles son esas diferencias?
return await promise
async function delay1Second() {
return (await delay(1000));
}
return promise
async function delay1Second() {
return delay(1000);
}
Según tengo entendido, el primero tendría manejo de errores dentro de la función asíncrona, y los errores saldrían de la Promesa de la función asíncrona. Sin embargo, el segundo requeriría un tic menos. ¿Es esto correcto?
Este fragmento es solo una función común para devolver una Promesa como referencia.
function delay(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
La mayoría de las veces, no hay diferencia observable entre return
y return await
. Ambas versiones delay1Second
tienen exactamente el mismo comportamiento observable (pero dependiendo de la implementación, la return await
versión puede usar un poco más de memoria porque Promise
se puede crear un objeto intermedio).
Sin embargo, como señaló @PitaJ, hay un caso en el que hay una diferencia: si return
o return await
está anidado en un bloque try
- catch
. Considere este ejemplo
async function rejectionWithReturnAwait () {
try {
return await Promise.reject(new Error())
} catch (e) {
return 'Saved!'
}
}
async function rejectionWithReturn () {
try {
return Promise.reject(new Error())
} catch (e) {
return 'Saved!'
}
}
En la primera versión, la función asíncrona espera la promesa rechazada antes de devolver su resultado, lo que hace que el rechazo se convierta en una excepción y catch
se alcance la cláusula; Por tanto, la función devolverá una promesa que se resolverá en la cadena "¡Guardado!".
La segunda versión de la función, sin embargo, devuelve la promesa rechazada directamente sin esperarla dentro de la función asíncrona , lo que significa que nocatch
se llama al caso y la persona que llama recibe el rechazo.
Como se mencionó en otras respuestas, es probable que exista un ligero beneficio de rendimiento al dejar que la promesa surja al devolverla directamente, simplemente porque no es necesario esperar el resultado primero y luego envolverlo con otra promesa nuevamente. Sin embargo, nadie ha hablado todavía de la optimización de las llamadas de cola .
La optimización de llamadas de cola , o “llamadas de cola adecuadas” , es una técnica que utiliza el intérprete para optimizar la pila de llamadas. Actualmente, no muchos tiempos de ejecución lo admiten todavía , aunque técnicamente es parte del estándar ES6 , pero es posible que se agregue soporte en el futuro, por lo que puede prepararse para eso escribiendo un buen código en el presente.
En pocas palabras, TCO (o PTC) optimiza la pila de llamadas al no abrir un nuevo marco para una función que otra función devuelve directamente. En cambio, reutiliza el mismo marco.
async function delay1Second() {
return delay(1000);
}
Dado que delay()
lo devuelve directamente delay1Second()
, los tiempos de ejecución que admiten PTC primero abrirán un marco para delay1Second()
(la función externa), pero luego, en lugar de abrir otro marco para delay()
(la función interna), simplemente reutilizarán el mismo marco que se abrió para la función externa. Esto optimiza la pila porque puede evitar un desbordamiento de la pila (jeje) con funciones recursivas muy grandes, por ejemplo, fibonacci(5e+25)
. Básicamente se convierte en un bucle, que es mucho más rápido.
PTC solo se habilita cuando la función interna se devuelve directamente . No se usa cuando el resultado de la función se modifica antes de devolverlo, por ejemplo, si tenía return (delay(1000) || null)
, o return await delay(1000)
.
Pero como dije, la mayoría de los tiempos de ejecución y navegadores aún no son compatibles con PTC, por lo que probablemente no haga una gran diferencia ahora, pero no estaría de más preparar su código para el futuro.
Lea más en esta pregunta: Node.js: ¿Existen optimizaciones para llamadas finales en funciones asíncronas?
Diferencia notable: el rechazo de la promesa se maneja en diferentes lugares
return somePromise
pasará algunaPromise al sitio de la llamada yawait
algunaPromise para liquidar en el sitio de la llamada (si hay alguna). Por lo tanto, si se rechaza alguna Promesa, no será manejada por el bloque catch local, sino por el bloque catch del sitio de llamada.
async function foo () {
try {
return Promise.reject();
} catch (e) {
console.log('IN');
}
}
(async function main () {
try {
let a = await foo();
} catch (e) {
console.log('OUT');
}
})();
// 'OUT'
return await somePromise
Primero esperará alguna promesa para establecerse localmente. Por lo tanto, el valor o la excepción se manejarán primero localmente. => El bloque catch local se ejecutará sisomePromise
se rechaza.
async function foo () {
try {
return await Promise.reject();
} catch (e) {
console.log('IN');
}
}
(async function main () {
try {
let a = await foo();
} catch (e) {
console.log('OUT');
}
})();
// 'IN'
Razón: return await Promise
espera tanto localmente como afuera, return Promise
espera solo afuera
Pasos detallados:
promesa de devolución
async function delay1Second() {
return delay(1000);
}
- llamar
delay1Second()
;
const result = await delay1Second();
- En el interior
delay1Second()
, la funcióndelay(1000)
devuelve una promesa inmediatamente con[[PromiseStatus]]: 'pending
. LlamémoslodelayPromise
.
async function delay1Second() {
return delayPromise;
// delayPromise.[[PromiseStatus]]: 'pending'
// delayPromise.[[PromiseValue]]: undefined
}
- Las funciones asíncronas envolverán su valor de retorno dentro
Promise.resolve()
( Fuente ). Comodelay1Second
es una función asíncrona, tenemos:
const result = await Promise.resolve(delayPromise);
// delayPromise.[[PromiseStatus]]: 'pending'
// delayPromise.[[PromiseValue]]: undefined
Promise.resolve(delayPromise)
regresadelayPromise
sin hacer nada porque la entrada ya es una promesa (ver MDN Promise.resolve ):
const result = await delayPromise;
// delayPromise.[[PromiseStatus]]: 'pending'
// delayPromise.[[PromiseValue]]: undefined
await
espera hasta que sedelayPromise
liquide.
- SI
delayPromise
se cumple con PromiseValue=1:
const result = 1;
- ELSE es
delayPromise
rechazado:
// jump to catch block if there is any
regreso aguarda promesa
async function delay1Second() {
return await delay(1000);
}
- llamar
delay1Second()
;
const result = await delay1Second();
- En el interior
delay1Second()
, la funcióndelay(1000)
devuelve una promesa inmediatamente con[[PromiseStatus]]: 'pending
. LlamémoslodelayPromise
.
async function delay1Second() {
return await delayPromise;
// delayPromise.[[PromiseStatus]]: 'pending'
// delayPromise.[[PromiseValue]]: undefined
}
- La espera local esperará hasta que
delayPromise
se resuelva.
- Caso 1 :
delayPromise
se cumple con PromiseValue=1:
async function delay1Second() {
return 1;
}
const result = await Promise.resolve(1); // let's call it "newPromise"
const result = await newPromise;
// newPromise.[[PromiseStatus]]: 'resolved'
// newPromise.[[PromiseValue]]: 1
const result = 1;
- Caso 2 :
delayPromise
se rechaza:
// jump to catch block inside `delay1Second` if there is any
// let's say a value -1 is returned in the end
const result = await Promise.resolve(-1); // call it newPromise
const result = await newPromise;
// newPromise.[[PromiseStatus]]: 'resolved'
// newPromise.[[PromiseValue]]: -1
const result = -1;
Glosario:
- Liquidar:
Promise.[[PromiseStatus]]
cambia depending
aresolved
orejected
En nuestro proyecto, decidimos utilizar siempre "espera de retorno". El argumento es que "el riesgo de olvidar agregar 'await' cuando más adelante se coloca un bloque try-catch alrededor de la expresión de retorno justifica tener el 'await' redundante ahora".
Esta es una pregunta difícil de responder, porque en la práctica depende de cómo babel
renderiza realmente su transpilador (probablemente) async/await
. Las cosas que están claras independientemente:
Ambas implementaciones deberían comportarse igual, aunque la primera implementación puede tener una menos
Promise
en la cadena.Especialmente si eliminas lo innecesario
await
, la segunda versión no requeriría ningún código adicional del transpilador, mientras que la primera sí.
Entonces, desde una perspectiva de depuración y rendimiento del código, la segunda versión es preferible, aunque solo un poco, mientras que la primera versión tiene un ligero beneficio de legibilidad, ya que indica claramente que devuelve una promesa.