Recientemente me he encontrado con un error bastante desagradable, en el que el código se estaba cargando <select>
dinámicamente a través de JavaScript. Este cargado dinámicamente <select>
tenía un valor preseleccionado. En IE6, ya teníamos código para arreglar lo seleccionado <option>
, porque a veces el valor del <select>
's selectedIndex
no estaba sincronizado con el atributo del <option>
' seleccionado ' index
, como se muestra a continuación:
field.selectedIndex = element.index;
Sin embargo, este código no estaba funcionando. Aunque el campo selectedIndex
se estaba configurando correctamente, el índice incorrecto terminaría siendo seleccionado. Sin embargo, si pegué una alert()
declaración en el momento adecuado, se seleccionaría la opción correcta. Pensando que esto podría ser algún tipo de problema de tiempo, probé algo al azar que había visto en el código antes:
var wrapFn = (function() {
var myField = field;
var myElement = element;
return function() {
myField.selectedIndex = myElement.index;
}
})();
setTimeout(wrapFn, 0);
Y esto funcionó!
Tengo una solución para mi problema, pero me incomoda no saber exactamente por qué esto soluciona mi problema. ¿Alguien tiene una explicación oficial? ¿Qué problema del navegador estoy evitando llamando a mi función "más tarde" setTimeout()
?
fuente
setTimeout(fn)
es igual quesetTimeout(fn, 0)
, por cierto.Respuestas:
En la pregunta, existía una condición de carrera entre:
Su código estaba constantemente ganando esta carrera e intentando establecer una selección desplegable antes de que el navegador estuviera listo, lo que significa que aparecería el error.
Esta carrera existió porque JavaScript tiene un solo hilo de ejecución que se comparte con la representación de la página. En efecto, ejecutar JavaScript bloquea la actualización del DOM.
Su solución fue:
Invocar
setTimeout
con una devolución de llamada y cero como segundo argumento programará la devolución de llamada para que se ejecute de forma asíncrona , después del menor retraso posible, que será de unos 10 ms cuando la pestaña tenga el foco y el hilo de ejecución de JavaScript no esté ocupado.La solución del OP, por lo tanto, fue retrasar unos 10 ms, la configuración del índice seleccionado. Esto le dio al navegador la oportunidad de inicializar el DOM, reparando el error.
Todas las versiones de Internet Explorer exhibían comportamientos extravagantes y, en ocasiones, este tipo de solución era necesaria. Alternativamente, podría haber sido un error genuino en la base de código del OP.
Vea a Philip Roberts hablar "¿Qué diablos es el ciclo de eventos?" Para una explicación más completa.
fuente
Prefacio:
Algunas de las otras respuestas son correctas, pero en realidad no ilustran cuál es el problema que se está resolviendo, así que creé esta respuesta para presentar esa ilustración detallada.
Como tal, estoy publicando un recorrido detallado de lo que hace el navegador y cómo
setTimeout()
ayuda el uso . Parece alargado, pero en realidad es muy simple y directo: lo acabo de detallar.ACTUALIZACIÓN: Hice un JSFiddle para demostrar en vivo la explicación a continuación: http://jsfiddle.net/C2YBE/31/ . Muchas gracias a @ThangChung por ayudar a arrancarlo.
ACTUALIZACIÓN2: en caso de que el sitio web JSFiddle muera o elimine el código, agregué el código a esta respuesta al final.
DETALLES :
Imagine una aplicación web con un botón de "hacer algo" y una división de resultados.
El
onClick
controlador para el botón "hacer algo" llama a una función "LongCalc ()", que hace 2 cosas:Hace un cálculo muy largo (por ejemplo, toma 3 min)
Imprime los resultados del cálculo en el resultado div.
Ahora, sus usuarios comienzan a probar esto, hacen clic en el botón "hacer algo", y la página se queda allí sin hacer nada durante 3 minutos, se inquietan, vuelven a hacer clic en el botón, esperan 1 minuto, no pasa nada, vuelven a hacer clic en el botón ...
El problema es obvio: desea un DIV "Estado", que muestre lo que está sucediendo. Veamos cómo funciona eso.
Entonces agrega un DIV "Estado" (inicialmente vacío) y modifica el
onclick
controlador (funciónLongCalc()
) para hacer 4 cosas:Rellene el estado "Calculando ... puede tardar ~ 3 minutos" en el estado DIV
Hace un cálculo muy largo (por ejemplo, toma 3 min)
Imprime los resultados del cálculo en el resultado div.
Rellene el estado "Cálculo realizado" en estado DIV
Y felizmente le das la aplicación a los usuarios para que la vuelvan a probar.
Vuelven a ti muy enojados. ¡Y explique que cuando hicieron clic en el botón, el estado DIV nunca se actualizó con el estado "Calculando ..."!
Se rasca la cabeza, pregunta en StackOverflow (o lee documentos o google) y se da cuenta del problema:
El navegador coloca todas sus tareas "TODO" (tareas de IU y comandos de JavaScript) resultantes de los eventos en una sola cola . Y, desafortunadamente, volver a dibujar el DIV "Estado" con el nuevo valor "Calculando ..." es un TODO separado que va al final de la cola.
Aquí hay un desglose de los eventos durante la prueba de su usuario, el contenido de la cola después de cada evento:
[Empty]
[Execute OnClick handler(lines 1-4)]
[Execute OnClick handler(lines 2-4), re-draw Status DIV with new "Calculating" value]
. Tenga en cuenta que si bien los cambios de DOM ocurren instantáneamente, para volver a dibujar el elemento DOM correspondiente, necesita un nuevo evento, desencadenado por el cambio de DOM, que se realizó al final de la cola .[Execute OnClick handler(lines 3-4), re-draw Status DIV with "Calculating" value]
.[Execute OnClick handler(line 4), re-draw Status DIV with "Calculating" value, re-draw result DIV with result]
.[Execute OnClick handler, re-draw Status DIV with "Calculating" value, re-draw result DIV with result; re-draw Status DIV with "DONE" value]
.return
desde elonclick
controlador sub. Quitamos el "Ejecutar controlador de OnClick" de la cola y comenzamos a ejecutar el siguiente elemento en la cola.Entonces, el problema subyacente es que el evento de nuevo sorteo para el DIV "Estado" se coloca en la cola al final, DESPUÉS del evento de "ejecución de la línea 2" que demora 3 minutos, por lo que el nuevo sorteo real no ocurre hasta DESPUÉS de que el cálculo esté hecho.
Al rescate viene el
setTimeout()
. Como ayuda Porque al llamar a través del código de ejecución prolongadasetTimeout
, en realidad crea 2 eventos: lasetTimeout
ejecución misma y (debido al tiempo de espera 0), entrada de cola separada para el código que se está ejecutando.Entonces, para solucionar su problema, modifique su
onClick
controlador para que sea DOS declaraciones (en una nueva función o simplemente un bloque dentroonClick
):Rellene el estado "Calculando ... puede tardar ~ 3 minutos" en el estado DIV
Ejecutar
setTimeout()
con 0 tiempo de espera y una llamada a laLongCalc()
función .LongCalc()
la función es casi la misma que la última vez, pero obviamente no tiene el estado "Calculando ..." actualización DIV de estado como primer paso; y en su lugar comienza el cálculo de inmediato.Entonces, ¿cómo se ve la secuencia de eventos y la cola ahora?
[Empty]
[Execute OnClick handler(status update, setTimeout() call)]
[Execute OnClick handler(which is a setTimeout call), re-draw Status DIV with new "Calculating" value]
.[re-draw Status DIV with "Calculating" value]
. La cola no tiene nada nuevo durante 0 segundos más.[re-draw Status DIV with "Calculating" value, execute LongCalc (lines 1-3)]
.[execute LongCalc (lines 1-3)]
. Tenga en cuenta que este evento de nuevo sorteo podría suceder ANTES de que suene la alarma, lo que funciona igual de bien.¡Hurra! ¡El DIV de estado acaba de actualizarse a "Calculando ..." antes de que comenzara el cálculo!
A continuación se muestra el código de ejemplo de JSFiddle que ilustra estos ejemplos: http://jsfiddle.net/C2YBE/31/ :
Código HTML:
Código JavaScript: (ejecutado
onDomReady
y puede requerir jQuery 1.9)fuente
Eche un vistazo al artículo de John Resig sobre cómo funcionan los temporizadores de JavaScript . Cuando establece un tiempo de espera, en realidad pone en cola el código asincrónico hasta que el motor ejecuta la pila de llamadas actual.
fuente
setTimeout()
compra algo de tiempo hasta que se cargan los elementos DOM, incluso si se establece en 0.Mira esto: setTimeout
fuente
La mayoría de los navegadores tienen un proceso llamado hilo principal, que es responsable de ejecutar algunas tareas de JavaScript, actualizaciones de la interfaz de usuario, por ejemplo: pintar, redibujar o refluir, etc.
Algunas tareas de ejecución de JavaScript y actualización de la interfaz de usuario se ponen en cola en la cola de mensajes del navegador, luego se envían al hilo principal del navegador para su ejecución.
Cuando se generan actualizaciones de la interfaz de usuario mientras el hilo principal está ocupado, las tareas se agregan a la cola de mensajes.
setTimeout(fn, 0);
agregue estofn
al final de la cola que se ejecutará. Programa una tarea que se agregará a la cola de mensajes después de un período de tiempo determinado.fuente
add this fn to the end of the queue
. Lo más importante es dóndesetTimeout
agrega exactamente esta función, el final de este ciclo de ciclo o el comienzo del siguiente ciclo de ciclo.Aquí hay respuestas conflictivas y votadas, y sin pruebas no hay forma de saber a quién creer. Aquí hay pruebas de que @DVK es correcto y @SalvadorDali es incorrecto. El último afirma:
El tiempo de espera mínimo de 4 ms es irrelevante para lo que está sucediendo. Lo que realmente sucede es que setTimeout empuja la función de devolución de llamada al final de la cola de ejecución. Si después de setTimeout (devolución de llamada, 0) tiene un código de bloqueo que tarda varios segundos en ejecutarse, la devolución de llamada no se ejecutará durante varios segundos, hasta que el código de bloqueo haya finalizado. Prueba este código:
Salida es:
fuente
Una razón para hacerlo es diferir la ejecución de código a un bucle de eventos posterior separado. Cuando se responde a un evento de navegador de algún tipo (clic del mouse, por ejemplo), a veces es necesario realizar operaciones solo después de que se procesa el evento actual. La
setTimeout()
instalación es la forma más sencilla de hacerlo.edite ahora que es 2015. Debo señalar que también hay
requestAnimationFrame()
, que no es exactamente lo mismo, pero está lo suficientemente cerca como parasetTimeout(fn, 0)
que valga la pena mencionarlo.fuente
Esta es una vieja pregunta con viejas respuestas. Quería agregar una nueva mirada a este problema y responder por qué sucede esto y no por qué es útil.
Entonces tienes dos funciones:
y luego llámalos en el siguiente orden
f1(); f2();
solo para ver que el segundo se ejecutó primero.Y aquí está la razón: no es posible tener
setTimeout
un retraso de tiempo de 0 milisegundos. El valor mínimo lo determina el navegador y no es 0 milisegundos. Históricamente, los navegadores establecen este mínimo en 10 milisegundos, pero las especificaciones HTML5 y los navegadores modernos lo establecen en 4 milisegundos.También de mozilla:
La información de PS se toma después de leer el siguiente artículo .
fuente
setTimeout(fn,0)
es útil.setTimeout
/setInterval
más de cinco niveles de profundidad; si no lo ha hecho, el mínimo es de 0 ms (con falta de máquinas de tiempo). Los documentos de Mozilla amplían eso para cubrir casos repetidos, no solo anidados (por lo tanto,setInterval
con un intervalo de 0 se reprogramará inmediatamente varias veces, luego se retrasará más después de eso), pero los usos simples desetTimeout
anidación mínima se pueden poner en cola de inmediato.Dado que se pasa una duración de
0
, supongo que es para eliminar el código pasadosetTimeout
del flujo de ejecución. Entonces, si se trata de una función que podría tomar un tiempo, no impedirá que se ejecute el código posterior.fuente
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 lamessage queue
. Cuandoevent loop
llega a este mensaje, crea unframe
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 (laon
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 elwindow
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.
fuente
La otra cosa que esto hace es empujar la invocación de la función al final de la pila, evitando un desbordamiento de la pila si está llamando recursivamente a una función. Esto tiene el efecto de un
while
bucle, pero permite que el motor de JavaScript active otros temporizadores asíncronos.fuente
push the function invocation to the bottom of the stack
. De quéstack
estás hablando es oscuro. Lo más importante es dóndesetTimeout
agrega exactamente esta función, el final de este ciclo de ciclo o el comienzo del siguiente ciclo de ciclo.Al llamar a setTimeout, le da tiempo a la página para reaccionar ante lo que sea que esté haciendo el usuario. Esto es particularmente útil para las funciones que se ejecutan durante la carga de la página.
fuente
Algunos otros casos en los que setTimeout es útil:
Desea dividir un ciclo o cálculo de larga duración en componentes más pequeños para que el navegador no parezca "congelarse" o diga "La secuencia de comandos en la página está ocupada".
Desea deshabilitar un botón de envío de formulario cuando se hace clic, pero si deshabilita el botón en el controlador onClick, el formulario no se enviará. setTimeout con un tiempo de cero hace el truco, permitiendo que el evento finalice, que el formulario comience a enviarse, luego su botón puede ser deshabilitado.
fuente
Las respuestas sobre los bucles de ejecución y la representación del DOM antes de que se complete algún otro código son correctas. Los tiempos de espera de cero segundos en JavaScript ayudan a que el código sea pseudo-multiproceso, aunque no lo sea.
Quiero agregar que el MEJOR valor para un tiempo de espera de segundo segundo de navegador cruzado / plataforma cruzada en JavaScript es en realidad unos 20 milisegundos en lugar de 0 (cero), porque muchos navegadores móviles no pueden registrar tiempos de espera menores de 20 milisegundos debido a limitaciones de reloj en chips AMD.
Además, los procesos de larga duración que no implican la manipulación del DOM deben enviarse ahora a Web Workers, ya que proporcionan una verdadera ejecución multiproceso de JavaScript.
fuente
setTimout en 0 también es muy útil en el patrón de configuración de una promesa diferida, que desea devolver de inmediato:
fuente
El problema era que estaba intentando realizar una operación de Javascript en un elemento no existente. El elemento aún no se había cargado y
setTimeout()
le da más tiempo para que se cargue un elemento de las siguientes maneras:setTimeout()
hace que el evento sea ansíncrono, por lo tanto, se ejecuta después de todo el código síncrono, lo que le da a su elemento más tiempo para cargar. Las devoluciones de llamada asincrónicas como la devolución de llamadasetTimeout()
se colocan en la cola de eventos y se colocan en la pila por el bucle de eventos después de que la pila de código síncrono esté vacía.setTimeout()
es a menudo un poco más alto (4-10 ms dependiendo del navegador). Este tiempo un poco más alto necesario para ejecutar lassetTimeout()
devoluciones de llamada es causado por la cantidad de 'ticks' (donde un tick empuja una devolución de llamada en la pila si la pila está vacía) del bucle de eventos. Debido a las razones de rendimiento y duración de la batería, la cantidad de tics en el bucle de eventos está restringida a una cierta cantidad inferior a 1000 veces por segundo.fuente
Javascript es una aplicación de un solo subproceso, por lo que no permite ejecutar la función simultáneamente, por lo que para lograr este evento se utilizan bucles. Entonces, exactamente qué setTimeout (fn, 0) hace que se ponga en búsqueda de tareas que se ejecuta cuando la pila de llamadas está vacía. Sé que esta explicación es bastante aburrida, así que te recomiendo que leas este video que te ayudará a ver cómo funcionan las cosas en el navegador. Mira este video: - https://www.youtube.com/watch?time_continue=392&v=8aGhZQkoFbQ
fuente