Estas dos respuestas mejor calificadas son incorrectas. Consulte la descripción de MDN sobre el modelo de concurrencia y el bucle de eventos , y debería quedar claro lo que está sucediendo (ese recurso MDN es una verdadera joya). Y simplemente usar setTimeout
puede agregar problemas inesperados en su código además de "resolver" este pequeño problema.
Lo que realmente está sucediendo aquí no es que "el navegador podría no estar listo todavía porque la concurrencia" o algo basado en "cada línea es un evento que se agrega al final de la cola".
El jsfiddle proporcionado por DVK de hecho ilustra un problema, pero su explicación no es correcta.
Lo que sucede en su código es que primero está adjuntando un controlador de click
eventos al evento en el #do
botón.
Luego, cuando hace clic en el botón, message
se crea una referencia a la función del controlador de eventos, que se agrega a la message queue
. Cuando event loop
llega a este mensaje, crea un frame
en la pila, con la llamada de función al controlador de eventos click en el jsfiddle.
Y aquí es donde se pone interesante. Estamos tan acostumbrados a pensar que Javascript es asíncrono que somos propensos a pasar por alto este pequeño hecho: cualquier marco debe ejecutarse, en su totalidad, antes de poder ejecutar el siguiente marco . Sin concurrencia, gente.
¿Qué significa esto? Significa que cada vez que se invoca una función desde la cola de mensajes, la bloquea hasta que se vacía la pila que genera. O, en términos más generales, bloquea hasta que la función ha regresado. Y bloquea todo , incluidas las operaciones de representación DOM, desplazamiento y demás. Si desea confirmación, solo intente aumentar la duración de la operación de ejecución larga en el violín (por ejemplo, ejecute el bucle externo 10 veces más), y notará que mientras se ejecuta, no puede desplazarse por la página. Si se ejecuta el tiempo suficiente, su navegador le preguntará si desea eliminar el proceso, porque está haciendo que la página no responda. El marco se está ejecutando y el bucle de eventos y la cola de mensajes se atascan hasta que finaliza.
Entonces, ¿por qué este efecto secundario del texto no se actualiza? Porque si bien ha cambiado el valor del elemento en el DOM, puede console.log()
cambiar su valor inmediatamente después de cambiarlo y ver que se ha cambiado (lo que muestra por qué la explicación de DVK no es correcta), el navegador está esperando que la pila se agote (la on
función del controlador para volver) y, por lo tanto, el mensaje para finalizar, de modo que eventualmente pueda ejecutar el mensaje que ha agregado el tiempo de ejecución como reacción a nuestra operación de mutación y para reflejar esa mutación en la interfaz de usuario .
Esto se debe a que en realidad estamos esperando que el código termine de ejecutarse. No hemos dicho "alguien busque esto y luego llame a esta función con los resultados, gracias, y ahora he terminado así que voy a regresar, haga lo que sea ahora", como solemos hacer con nuestro Javascript asincrónico basado en eventos. Ingresamos una función de controlador de eventos de clic, actualizamos un elemento DOM, llamamos a otra función, la otra función funciona durante mucho tiempo y luego regresa, luego actualizamos el mismo elemento DOM y luego regresamos de la función inicial, vaciando efectivamente la pila. Y luego el navegador puede llegar al siguiente mensaje en la cola, que bien podría ser un mensaje generado por nosotros al desencadenar algún tipo de evento interno de "mutación DOM".
La interfaz de usuario del navegador no puede (o elige no) actualizar la interfaz de usuario hasta que el marco actualmente en ejecución se haya completado (la función ha regresado). Personalmente, creo que esto es más bien por diseño que por restricción.
¿Por qué funciona la setTimeout
cosa entonces? Lo hace, porque elimina efectivamente la llamada a la función de ejecución prolongada de su propio marco, programando que se ejecute más adelante en el window
contexto, para que pueda regresar de inmediato y permitir que la cola de mensajes procese otros mensajes. Y la idea es que el mensaje de "actualización" de la interfaz de usuario que hemos activado en Javascript al cambiar el texto en el DOM ahora está por delante del mensaje en cola para la función de ejecución prolongada, de modo que la actualización de la interfaz de usuario ocurre antes de que bloqueemos por mucho tiempo.
Tenga en cuenta que a) la función de ejecución prolongada todavía bloquea todo cuando se ejecuta, yb) no se garantiza que la actualización de la interfaz de usuario esté realmente por delante en la cola de mensajes. En mi navegador Chrome de junio de 2018, un valor de 0
no "soluciona" el problema que demuestra el violín: 10 sí. De hecho, estoy un poco sofocado por esto, porque me parece lógico que el mensaje de actualización de la interfaz de usuario se ponga en cola antes que él, ya que su disparador se ejecuta antes de programar la función de ejecución larga para que se ejecute "más tarde". Pero tal vez hay algunas optimizaciones en el motor V8 que pueden interferir, o tal vez mi comprensión es insuficiente.
Bien, entonces, ¿cuál es el problema con el uso setTimeout
y cuál es una mejor solución para este caso en particular?
En primer lugar, el problema con el uso setTimeout
de cualquier controlador de eventos como este, para tratar de aliviar otro problema, es propenso a meterse con otro código. Aquí hay un ejemplo de la vida real de mi trabajo:
Un colega, en una comprensión mal informada sobre el bucle de eventos, trató de "enhebrar" Javascript al usar algún código de representación de plantilla setTimeout 0
para su representación. Ya no está aquí para preguntar, pero puedo suponer que quizás insertó temporizadores para medir la velocidad de representación (que sería la inmediatez de las funciones) y descubrió que el uso de este enfoque generaría respuestas increíblemente rápidas de esa función.
El primer problema es obvio; no puede enhebrar javascript, por lo que no gana nada aquí mientras agrega ofuscación. En segundo lugar, ahora ha separado efectivamente la representación de una plantilla de la pila de posibles oyentes de eventos que podrían esperar que esa plantilla se haya renderizado, aunque es muy posible que no lo haya sido. El comportamiento real de esa función ahora no era determinista, como lo era, sin saberlo, cualquier función que la ejecutara o dependiera de ella. Puede hacer conjeturas informadas, pero no puede codificar adecuadamente su comportamiento.
La "solución" al escribir un nuevo controlador de eventos que dependía de su lógica también debía usarse setTimeout 0
. Pero, eso no es una solución, es difícil de entender, y no es divertido depurar errores causados por un código como este. A veces no hay ningún problema, otras veces falla constantemente, y de nuevo, a veces funciona y se rompe esporádicamente, dependiendo del rendimiento actual de la plataforma y de cualquier otra cosa que suceda en ese momento. Es por eso que personalmente recomendaría no usar este truco ( es un truco, y todos deberíamos saber que lo es), a menos que realmente sepa lo que está haciendo y cuáles son las consecuencias.
Pero lo que podemos que hacer en su lugar? Bueno, como sugiere el artículo de MDN al que se hace referencia, divida el trabajo en varios mensajes (si puede) para que otros mensajes que estén en cola se puedan intercalar con su trabajo y ejecutar mientras se ejecuta, o use un trabajador web, que puede ejecutar junto con su página y devolver resultados cuando haya terminado con sus cálculos.
Ah, y si estás pensando: "Bueno, ¿no podría simplemente devolver una llamada en la función de ejecución prolongada para que sea asíncrona?", Entonces no. La devolución de llamada no lo hace asíncrono, aún tendrá que ejecutar el código de ejecución prolongada antes de llamar explícitamente a su devolución de llamada.