w3schools una termine que promises promesas google funcion examples esperar anidadas javascript promise es6-promise

una - ¿Cuál es el orden de ejecución en las promesas javascript?



promises examples js (2)

Me gustaría explicarme el orden de ejecución del siguiente fragmento de código que utiliza promesas de javascript.

Promise.resolve(''A'') .then(function(a){console.log(2, a); return ''B'';}) .then(function(a){ Promise.resolve(''C'') .then(function(a){console.log(7, a);}) .then(function(a){console.log(8, a);}); console.log(3, a); return a;}) .then(function(a){ Promise.resolve(''D'') .then(function(a){console.log(9, a);}) .then(function(a){console.log(10, a);}); console.log(4, a);}) .then(function(a){ console.log(5, a);}); console.log(1); setTimeout(function(){console.log(6)},0);

El resultado es:

1 2 "A" 3 "B" 7 "C" 4 "B" 8 undefined 9 "D" 5 undefined 10 undefined 6

Tengo curiosidad por la orden de ejecución 1 2 3 7 ... no los valores ''A'', ''B'' ...

Tengo entendido que si se resuelve una promesa, la función ''entonces'' se coloca en la cola de eventos del navegador. Así que mi expectativa era 1 2 3 4 ...

@ jfriend00 Gracias, muchas gracias por las explicaciones detalladas! ¡Es realmente una enorme cantidad de trabajo!


El motor de JavaScript del navegador tiene algo llamado "bucle de eventos". Solo hay un hilo de código JavaScript ejecutándose a la vez. Cuando se hace clic en un botón o se completa una solicitud AJAX o algo asincrónico, se coloca un nuevo evento en el bucle de eventos. El navegador ejecuta estos eventos uno a la vez.

Lo que estás viendo aquí es que ejecutas código que se ejecuta de forma asíncrona. Cuando se completa el código asíncrono, se agrega un evento apropiado al bucle de eventos. El orden en el que se agregan los eventos depende del tiempo que lleve completar cada operación asíncrona.

Eso significa que si está usando algo como AJAX, donde no tiene control sobre el orden en que se completarán las solicitudes, sus promesas pueden ejecutarse en un orden diferente cada vez.


Comentarios

En primer lugar, ejecutar promesas dentro de un controlador .then() y NO devolver esas promesas desde la devolución de llamada .then() crea una secuencia de promesas sin .then() completamente nueva que no está sincronizada con las promesas principales de ninguna manera. Por lo general, esto es un error y, de hecho, algunos motores de promesa realmente advierten cuando haces eso porque casi nunca es el comportamiento deseado. La única vez que uno querría hacer eso es cuando está haciendo algún tipo de operación de disparo y olvido en el que no le importan los errores y no le importa la sincronización con el resto del mundo.

Por lo tanto, todas las promesas Promise.resolve() dentro de .then() controladores crean nuevas cadenas Promise que se ejecutan independientemente de la cadena principal. No tienes un comportamiento determinado. Es como lanzar cuatro llamadas ajax en paralelo. No sabes cuál será el primero en completar. Ahora, dado que todo su código dentro de los manejadores Promise.resolve() es sincrónico (ya que este no es el código del mundo real), es posible que tenga un comportamiento consistente, pero ese no es el punto de diseño de las promesas, así que no lo haría. t pasa mucho tiempo tratando de averiguar qué Cadena de Promesa que solo ejecuta código síncrono va a terminar primero. En el mundo real, no importa porque si el orden importa, entonces no dejarás las cosas al azar de esta manera.

Resumen

  1. Todos los .then() se llaman de forma asíncrona después de que finalice el subproceso de ejecución actual (como dice la especificación Promesas / A +, cuando el motor JS vuelve a "código de plataforma"). Esto es cierto incluso para las promesas que se resuelven de forma síncrona, como Promise.resolve().then(...) . Esto se hace para la coherencia de la programación, de modo que un controlador .then() se llame sistemáticamente de forma asíncrona sin importar si la promesa se resuelve de inmediato o más tarde. Esto evita algunos errores de tiempo y facilita que el código de llamada vea una ejecución asíncrona consistente.

  2. No hay ninguna especificación que determine el orden relativo de setTimeout() comparación con los .then() programados .then() si ambos están en cola y listos para ejecutarse. En su implementación, un controlador .then() pendiente siempre se ejecuta antes que un setTimeout() pendiente, pero la especificación de Promesas / A + dice que esto no es determinado. Dice que los manejadores .then() pueden programarse de .then() maneras, algunas de las cuales se ejecutarán antes de las setTimeout() pendientes a setTimeout() y algunas de las cuales podrían ejecutarse después de las setTimeout() pendientes a setTimeout() . Por ejemplo, la especificación Promises / A + permite que los .then() se programen con setImmediate() que se ejecutaría antes de las llamadas de setTimeout() pendientes o con setTimeout() que se ejecutaría después de las llamadas de setTimeout() pendientes. Por lo tanto, su código no debe depender de ese orden en absoluto.

  3. Múltiples cadenas de Promesa independientes no tienen un orden de ejecución predecible y no puede confiar en ningún orden en particular. Es como disparar cuatro llamadas ajax en paralelo, donde no sabes cuál se completará primero.

  4. Si el orden de ejecución es importante, no cree una carrera que dependa de los detalles de implementación minuciosos. En su lugar, vincular cadenas de promesa para forzar una orden de ejecución particular.

  5. Por lo general, no desea crear cadenas de promesa independientes dentro de un controlador .then() que no se devuelven desde el controlador. Esto suele ser un error excepto en casos raros de incendio y olvídate sin manejo de errores.

Línea por línea de análisis

Entonces, aquí hay un análisis de su código. Agregué números de línea y limpié la muesca para facilitar el debate:

1 Promise.resolve(''A'').then(function (a) { 2 console.log(2, a); 3 return ''B''; 4 }).then(function (a) { 5 Promise.resolve(''C'').then(function (a) { 6 console.log(7, a); 7 }).then(function (a) { 8 console.log(8, a); 9 }); 10 console.log(3, a); 11 return a; 12 }).then(function (a) { 13 Promise.resolve(''D'').then(function (a) { 14 console.log(9, a); 15 }).then(function (a) { 16 console.log(10, a); 17 }); 18 console.log(4, a); 19 }).then(function (a) { 20 console.log(5, a); 21 }); 22 23 console.log(1); 24 25 setTimeout(function () { 26 console.log(6) 27 }, 0);

La línea 1 inicia una cadena de promesa y le adjunta un controlador .then() . Como Promise.resolve() resuelve de inmediato, la biblioteca Promise programará el primer controlador .then() para que se ejecute después de que este hilo de Javascript finalice. En las bibliotecas de promesas compatibles con Promises / A +, todos los .then() se llaman de forma asíncrona cuando finaliza el subproceso de ejecución actual y cuando JS vuelve al bucle de eventos. Esto significa que cualquier otro código síncrono en este hilo, como su console.log(1) se ejecutará a continuación, que es lo que ve.

Todos los demás manejadores .then() en el nivel superior ( líneas 4, 12, 19 ) encadenan después del primero y se ejecutarán solo después de que el primero tenga su turno. Ellos están esencialmente en cola en este punto.

Como setTimeout() también se encuentra en este hilo inicial de ejecución, se ejecuta y, por lo tanto, se programa un temporizador.

Ese es el final de la ejecución síncrona. Ahora, el motor JS comienza a ejecutar cosas que están programadas en la cola de eventos.

Por lo que sé, no hay ninguna garantía de que setTimeout(fn, 0) primero un setTimeout(fn, 0) o .then() que estén programados para ejecutarse justo después de este hilo de ejecución. .then() manejadores se consideran "micro-tareas", por lo que no me sorprende que se ejecuten primero antes del setTimeout() . Pero, si necesita un pedido en particular, debe escribir un código que garantice un pedido en lugar de confiar en este detalle de implementación.

De todos modos, el controlador .then() definido en la línea 1 se ejecuta a continuación. Por lo tanto, se ve la salida 2 "A" de ese console.log(2, a) .

A continuación, dado que el controlador .then() anterior devolvió un valor simple, esa promesa se considera resuelta por lo que el controlador .then() definido en la línea 4 se ejecuta. Aquí es donde está creando otra cadena de promesa independiente e introduciendo un comportamiento que suele ser un error.

La línea 5 , crea una nueva cadena de Promesa. Resuelve esa promesa inicial y luego programa dos .then() para que se ejecuten cuando se realiza el subproceso de ejecución actual. En ese hilo de ejecución actual está la console.log(3, a) en la línea 10, por lo que verá eso a continuación. Luego, este hilo de ejecución finaliza y vuelve al programador para ver qué ejecutar a continuación.

Ahora tenemos varios manejadores .then() en la cola que esperan para ejecutarse a continuación. Está el que acabamos de programar en la línea 5 y el siguiente en la cadena de nivel superior en la línea 12. Si hubiera hecho esto en la línea 5 :

return Promise.resolve.then(...)

entonces habrían vinculado estas promesas y se coordinarían en secuencia. Pero, al no devolver el valor de promesa, comenzó una nueva cadena de promesa que no está coordinada con la promesa externa de nivel superior. En su caso particular, el programador de promesas decide ejecutar el controlador .then() más profundamente anidado a continuación. Honestamente, no sé si esto es por especificación, por convención o simplemente por un detalle de implementación de un motor de promesa frente a otro. Yo diría que si el orden es crítico para usted, entonces debe forzar un orden vinculando promesas en un orden específico en lugar de confiar en quién gana la carrera para correr primero.

De todos modos, en su caso, es una carrera de programación y el motor que está ejecutando decide ejecutar el controlador .then() interno que se define en la línea 5 a continuación y, por lo tanto, verá las 7 "C" especificadas en la línea 6 . Entonces, no devuelve nada, por lo que el valor resuelto de esta promesa se vuelve undefined .

De vuelta en el programador, ejecuta el controlador .then() en la línea 12 . Esto es nuevamente una carrera entre ese controlador .then() y el que está en la línea 7, que también está esperando para correr. No sé por qué elige uno sobre el otro aquí, excepto para decir que puede ser indeterminado o variar según el motor de promesa porque el código no especifica el orden. En cualquier caso, el controlador .then() en la línea 12 comienza a ejecutarse. Eso nuevamente crea una nueva línea de cadena de promesa independiente o no sincronizada la anterior. Programa un controlador .then() nuevo y luego obtiene la 4 "B" del código síncrono en ese controlador .then() . Todo el código síncrono se realiza en ese controlador, por lo que ahora vuelve al programador para la siguiente tarea.

De vuelta en el programador, decide ejecutar el controlador .then() en la línea 7 y obtienes 8 undefined . La promesa no está undefined porque el anterior .then() en esa cadena no devolvió nada, por lo que su valor de retorno undefined estaba undefined , por lo que ese es el valor resuelto de la cadena de promesa en ese momento.

En este punto, la salida hasta ahora es:

1 2 "A" 3 "B" 7 "C" 4 "B" 8 undefined

Una vez más, todo el código síncrono está listo para que vuelva al programador y decida ejecutar el controlador .then() definido en la línea 13 . Se ejecuta y se obtiene la salida 9 "D" y luego vuelve al programador nuevamente.

Consistente con la cadena Promise.resolve() anidada anteriormente, el programa elige ejecutar el siguiente controlador .then() externo definido en la línea 19 . Se ejecuta y se obtiene la salida 5 undefined . Nuevamente está undefined porque el anterior .then() en esa cadena no devolvió un valor, por lo que el valor resuelto de la promesa undefined estaba undefined .

Como este punto, la salida hasta ahora es:

1 2 "A" 3 "B" 7 "C" 4 "B" 8 undefined 9 "D" 5 undefined

En este punto, solo hay un controlador .then() programado para ejecutarse, por lo que ejecuta el que está definido en la línea 15 y obtiene el resultado 10 undefined continuación.

Luego, por último, el setTimeout() se ejecuta y la salida final es:

1 2 "A" 3 "B" 7 "C" 4 "B" 8 undefined 9 "D" 5 undefined 10 undefined 6

Si uno intentara predecir exactamente el orden en que se ejecutaría, entonces habría dos preguntas principales.

  1. Cómo están pendientes las .then() controladores frente a setTimeout() que también están pendientes.

  2. ¿Cómo decide el motor de promesa dar prioridad a varios .then() que están todos esperando para ejecutarse? Por sus resultados con este código no es FIFO.

Para la primera pregunta, no sé si esto es por especificación o simplemente una opción de implementación aquí en el motor de promesa / JS, pero la implementación que reportó parece priorizar todos los .then() pendientes antes de cualquier setTimeout() llamadas Su caso es un poco extraño porque no tiene llamadas de API asíncronas reales aparte de especificar los .then() . Si tuvo alguna operación asíncrona que realmente tomó tiempo real para ejecutarse al comienzo de esta cadena de promesa, entonces su setTimeout() se ejecutaría antes que el controlador .then() en la operación asíncrona real solo porque la operación asíncrona real toma tiempo real ejecutar. Por lo tanto, este es un poco de un ejemplo artificial y no es el caso de diseño habitual para código real.

Para la segunda pregunta, he visto una discusión que discute cómo deben priorizarse los manejadores .then() en diferentes niveles de anidación. No sé si esa discusión alguna vez se resolvió en una especificación o no. Prefiero codificar de manera que ese nivel de detalle no me importe. Si me importa el orden de mis operaciones asíncronas, entonces vinculo mis cadenas de promesa para controlar el orden y este nivel de detalle de implementación no me afecta de ninguna manera. Si no me importa el pedido, entonces no me importa el pedido, por lo que, de nuevo, el nivel de detalle de implementación no me afecta. Incluso si esto estaba en alguna especificación, parece ser el tipo de detalle en el que no se debe confiar en muchas implementaciones diferentes (diferentes navegadores, diferentes motores de promesa) a menos que lo haya probado en todos los lugares donde iba a ejecutar. Por lo tanto, recomendaría no confiar en un orden de ejecución específico cuando tenga cadenas de promesa no sincronizadas.

Puede hacer que el orden sea determinado al 100% simplemente vinculando todas sus cadenas de promesas como esta (devolviendo promesas internas para que estén vinculadas a la cadena principal):

Promise.resolve(''A'').then(function (a) { console.log(2, a); return ''B''; }).then(function (a) { var p = Promise.resolve(''C'').then(function (a) { console.log(7, a); }).then(function (a) { console.log(8, a); }); console.log(3, a); // return this promise to chain to the parent promise return p; }).then(function (a) { var p = Promise.resolve(''D'').then(function (a) { console.log(9, a); }).then(function (a) { console.log(10, a); }); console.log(4, a); // return this promise to chain to the parent promise return p; }).then(function (a) { console.log(5, a); }); console.log(1); setTimeout(function () { console.log(6) }, 0);

Esto da el siguiente resultado en Chrome:

1 2 "A" 3 "B" 7 "C" 8 undefined 4 undefined 9 "D" 10 undefined 5 undefined 6

Y, dado que todas las promesas se han encadenado, la orden de promesas está definida por el código. Lo único que queda como detalle de implementación es el tiempo de setTimeout() que, como en su ejemplo, es el último, después de todos los .then() pendientes.

Editar:

Al examinar la especificación de Promesas / A + , encontramos esto:

2.2.4 onFulfilled o onRejected no se debe llamar hasta que la pila de contexto de ejecución solo contenga código de plataforma. [3.1].

....

3.1 Aquí, “código de plataforma” significa el código de implementación del motor, el entorno y la promesa. En la práctica, este requisito garantiza que onFulfilled y onRejected se ejecuten de forma asíncrona, después del giro del evento en el que se llama, y ​​con una pila nueva. Esto se puede implementar con un mecanismo de "macro-tarea" como setTimeout o setImmediate, o con un mecanismo de "micro-tarea" como MutationObserver o process.nextTick. Como la implementación de la promesa se considera un código de plataforma, puede contener una cola de planificación de tareas o "trampolín" en la que se llama a los manejadores.

Esto indica que los manejadores .then() deben ejecutarse de forma asíncrona después de que la pila de llamadas vuelva al código de la plataforma, pero deja a la implementación completamente como hacerlo exactamente si se realiza con una macro-tarea como setTimeout() o un process.nextTick() similar a micro-tarea process.nextTick() . Por lo tanto, según esta especificación, no está determinado y no se debe confiar en él.

No encuentro información sobre las tareas de macro, las tareas de micro o el tiempo de promesa .then() en relación con setTimeout() en la especificación ES6. Tal vez esto no sea sorprendente ya que setTimeout() sí no forma parte de la especificación ES6 (es una función de entorno de host, no una característica de idioma).

No he encontrado ninguna especificación para respaldar esto, pero las respuestas a esta pregunta La diferencia entre microtask y macrotask dentro de un contexto de bucle de eventos explica cómo las cosas tienden a funcionar en los navegadores con macro-tareas y micro-tareas.

Para su información, si desea obtener más información sobre las micro-tareas y las macro-tareas, aquí hay un interesante artículo de referencia sobre el tema: tareas, microtasks, colas y horarios .