Javascript promete curiosidad

96

Cuando llamo a esta promesa, la salida no coincide con la secuencia de llamadas a funciones. El .thenviene antes del .catch, aunque la promesa con .thense llama después. ¿Cuál es la razón para eso?

const verifier = (a, b) =>
  new Promise((resolve, reject) => (a > b ? resolve(true) : reject(false)));

verifier(3, 4)
  .then((response) => console.log("response: ", response))
  .catch((error) => console.log("error: ", error));

verifier(5, 4)
  .then((response) => console.log("response: ", response))
  .catch((error) => console.log("error: ", error));

salida

node promises.js
response: true
error: false
Gustavo Alves
fuente
34
Nunca debe confiar en los tiempos entre cadenas independientes de promesas.
Bergi

Respuestas:

136

Esta es una pregunta interesante para llegar al fondo.

Cuando haces esto:

verifier(3,4).then(...)

que devuelve una nueva promesa que requiere otro ciclo de regreso al bucle de eventos antes de que la promesa recién rechazada pueda ejecutar el .catch()controlador que sigue. Ese ciclo adicional da la siguiente secuencia:

verifier(5,4).then(...)

una oportunidad de ejecutar su .then()controlador antes que la línea anterior .catch()porque ya estaba en la cola antes de que el .catch()controlador de la primera entra en la cola y los elementos se ejecutan desde la cola en orden FIFO.


Tenga en cuenta que si usa el .then(f1, f2)formulario en lugar del .then().catch(), se ejecuta cuando lo espera porque no hay ninguna promesa adicional y, por lo tanto, no hay ninguna marca adicional involucrada:

const verifier = (a, b) =>
  new Promise((resolve, reject) => (a > b ? resolve(true) : reject(false)));

verifier(3, 4)
  .then((response) => console.log("response (3,4): ", response),
        (error) => console.log("error (3,4): ", error)
  );

verifier(5, 4)
  .then((response) => console.log("response (5,4): ", response))
  .catch((error) => console.log("error (5,4): ", error));

Tenga en cuenta que también etiqueté todos los mensajes para que pueda ver de qué verifier()llamada provienen, lo que hace que sea mucho más fácil leer el resultado.


ES6 Spec sobre pedido de devolución de llamada de promesa y explicación más detallada

La especificación ES6 nos dice que los "trabajos" de promesa (como llama a una devolución de llamada desde a .then()o .catch()) se ejecutan en orden FIFO según el momento en que se insertan en la cola de trabajos. No nombra específicamente a FIFO, pero especifica que los nuevos trabajos se insertan al final de la cola y los trabajos se ejecutan desde el principio de la cola. Eso implementa la ordenación FIFO.

PerformPromiseThen (que ejecuta la devolución de llamada desde .then()) conducirá a EnqueueJob, que es la forma en que se programa la ejecución del controlador de resolución o rechazo. EnqueueJob especifica que el trabajo pendiente se agrega al final de la cola de trabajos. Luego, la operación NextJob extrae el elemento del frente de la cola. Esto asegura el orden FIFO en los trabajos de servicio de la cola de trabajos de Promise.

Entonces, en el ejemplo de la pregunta original, obtenemos las devoluciones de llamada para la verifier(3,4)promesa y la verifier(5,4)promesa insertadas en la cola de trabajos en el orden en que se ejecutaron porque ambas promesas originales se cumplieron. Luego, cuando el intérprete vuelve al bucle de eventos, primero retoma el verifier(3,4)trabajo. Esa promesa se rechaza y no hay devolución de llamada para eso en el verifier(3,4).then(...). Entonces, lo que hace es rechazar la promesa que verifier(3,4).then(...)regresó y que hace que el verifier(3,4).then(...).catch(...)controlador se inserte en jobQueue.

Luego, vuelve al bucle de eventos y el siguiente trabajo que extrae de jobQueue es el verifier(5, 4)trabajo. Eso tiene una promesa resuelta y un controlador de resolución, por lo que llama a ese controlador. Esto hace response (5,4):que se muestre la salida.

Luego, vuelve al bucle de eventos y el siguiente trabajo que extrae de jobQueue es el verifier(3,4).then(...).catch(...)trabajo donde lo ejecuta y esto hace error (3,4)que se muestre el resultado.

Es porque .catch()en la 1ª cadena hay un nivel de promesa más profundo en su cadena que .then()en la 2ª cadena que provoca el pedido que informó. Y es porque las cadenas de promesa se atraviesan de un nivel al siguiente a través de la cola de trabajos en orden FIFO, no de forma sincrónica.


Recomendación general sobre la confianza en este nivel de detalle de la programación

Para su información, en general, trato de escribir código que no dependa de este nivel de conocimiento detallado del tiempo. Si bien es curioso y ocasionalmente útil de entender, es un código frágil ya que un simple cambio aparentemente inocuo en el código puede conducir a un cambio en el tiempo relativo. Entonces, si el tiempo es crítico entre dos cadenas como esta, entonces prefiero escribir el código de una manera que fuerce el tiempo de la manera que quiero en lugar de confiar en este nivel de comprensión detallada.

jfriend00
fuente
Para ser más específico, este comportamiento exacto no está documentado en ninguna parte de la especificación de promesas, lo que lo convierte en un detalle de implementación. Puede obtener un comportamiento diferente entre intérpretes (por ejemplo, Node.js vs Edge vs Firefox) o entre versiones de intérpretes (por ejemplo, Node 12 vs Node 14). La especificación simplemente dice que las promesas se procesan de forma asincrónica para evitar el código zalgo (que en mi humilde opinión fue un error por cierto porque estaba motivado por personas que estaban haciendo preguntas como esta que querían depender de la sincronización del código potencialmente asincrónico)
slebetman
@slebetman: ¿no está documentado que las devoluciones de llamada de promesa de promesas separadas se denominan FIFO en función de cuándo se insertaron en la cola y no pueden ejecutarse hasta el siguiente tick? Parece que el pedido FIFO es todo lo que se requiere aquí porque .then()tiene que devolver una nueva promesa que debe resolver / rechazar asincrónicamente en un futuro tick que es lo que conduce a este pedido. ¿Conoce alguna implementación que no utilice el orden FIFO de las devoluciones de llamada de la competencia?
jfriend00
3
@slebetman Promises / A + no lo especifica. ES6 lo especifica. (Sin awaitembargo, ES11 cambió el comportamiento de ).
Bergi
De la especificación ES6 en el orden de cola. PerformPromiseThenconducirá a EnqueueJobcuál es la forma en que se programa la llamada al controlador de resolución o rechazo. EnqueueJob especifica que el trabajo pendiente se agrega al final de la cola de trabajos. Luego, la operación NextJob extrae el elemento del principio de la cola. Esto asegura el orden FIFO en la cola de trabajos de Promise.
jfriend00
@Bergi ¿En qué consiste este cambio awaiten ES11? Basta con un enlace. ¡¡Gracias!!
Pedro A
49

Promise.resolve()
  .then(() => console.log('a1'))
  .then(() => console.log('a2'))
  .then(() => console.log('a3'))
Promise.resolve()
  .then(() => console.log('b1'))
  .then(() => console.log('b2'))
  .then(() => console.log('b3'))

En lugar de la salida a1, a2, a3, b1, b2, b3, verá a1, b1, a2, b2, a3, b3 por la misma razón: cada entonces devuelve una promesa y va al final del ciclo de eventos. cola. Entonces podemos ver esta "carrera de promesas". Lo mismo ocurre cuando hay algunas promesas anidadas.

Tarukami
fuente