¿Por qué setTimeout (fn, 0) es a veces útil?

Recientemente, encontré un error bastante desagradable en el que el código se cargó de forma dinámica a través de JavaScript. Este <select> cargado dinámicamente tenía un valor preseleccionado. En IE6, ya teníamos un código para corregir la <option> seleccionada, porque a veces el valor <select> selectedIndex no se sincronizaría con el atributo del index <option> seleccionado, como se muestra a continuación:

 field.selectedIndex = element.index; 

Sin embargo, este código no funcionó. Aunque el campo selectedIndex se configuró correctamente, el resultado sería un índice incorrecto. Sin embargo, si inserto la instrucción alert() en el momento correcto, se seleccionará la correcta. Pensando que podría ser algún tipo de problema de tiempo, intenté algo al azar que vi en el código antes:

 var wrapFn = (function() { var myField = field; var myElement = element; return function() { myField.selectedIndex = myElement.index; } })(); setTimeout(wrapFn, 0); 

¡Y funcionó!

Tengo una solución para mi problema, pero me avergüenza no saber exactamente por qué esto soluciona mi problema. ¿Alguien tiene una explicación formal? ¿Qué problema de navegador evito al llamar a mi función "más tarde" usando setTimeout() ?

621
23 апр. fijado por Daniel Lew 23 de abril 2009-04-23 00:46 '09 a las 0:46 2009-04-23 00:46
@ 14 respuestas

Esto funciona porque realizas multitarea conjunta.

El navegador tiene que hacer varias cosas casi de inmediato, y solo una de ellas es la ejecución de JavaScript. Pero una de las cosas para las que se suele usar javascript es pedirle al navegador que cree un elemento de visualización. A menudo se asume que esto se realiza de forma síncrona (especialmente porque JavaScript no se ejecuta en paralelo), pero no hay garantía de que este sea el caso, y JavaScript no tiene un mecanismo de espera bien definido.

La solución es "pausar" la ejecución de JavaScript para que los subprocesos de procesamiento queden atrapados. Y este es el efecto que hace setTimeout() con un tiempo de espera de 0 . Esto es similar al resultado de un subproceso / proceso en C. Aunque parece que se "lanzó de inmediato", en realidad le da al navegador la oportunidad de terminar de hacer cosas que no están en JavaScript y que están esperando su finalización antes de embarcarse en esta nueva parte de JavaScript.

border=0

(En realidad, setTimeout() reordena el nuevo JavaScript al final de la cola de ejecución. Consulte los comentarios para obtener una explicación más detallada).

IE6 resulta ser más propenso a este error, pero lo he visto en versiones anteriores de Mozilla y en Firefox.

607
23 апр. La respuesta es dada por staticsan el 23 de abril. 2009-04-23 03:14 '09 a las 3:14 2009-04-23 03:14

Introducción:

NOTA IMPORTANTE: mientras es más aprobado y aceptado, la respuesta aceptada de @staticsan en realidad NO ES CORRECTA! - Vea el comentario de David Mulder para una explicación de las razones.

Algunas de las otras respuestas son correctas, pero en realidad no ilustran que el problema se está resolviendo, así que creé esta respuesta para proporcionar esta ilustración detallada.

Como tal, publico una descripción detallada de lo que hace el navegador y cómo usar setTimeout() ayuda . Parece largo, pero en realidad muy simple y claro, lo describí con gran detalle.

ACTUALIZACIÓN: Hice JSFiddle para el programa; explique a continuación: http://jsfiddle.net/C2YBE/31/ . Muchas gracias a @ThangChung por ayudar a lanzarlo.

ACTUALIZACIÓN2: En caso de que el sitio web de JSFiddle muera o elimine el código, agregué un código a esta respuesta al final.


DETALLES

Presente una aplicación web con un botón "hacer algo" y un resultado div.

El controlador onClick para el botón "hacer algo" llama a la función "LongCalc ()", que hace 2 cosas:

  • Hace un cálculo muy largo (por ejemplo, toma 3 minutos)

  • Imprime los resultados del cálculo como resultado de la div.

Ahora los usuarios comenzarán a probarlo, hacen clic en el botón "hacer algo" y la página se queda allí sin ver nada durante 3 minutos, se ponen inquietos, vuelven a presionar el botón, esperan 1 minuto, no pasa nada, vuelven a presionar el botón ...

El problema es obvio: se necesita un DIV de "estado" que muestre lo que está sucediendo. Vamos a ver cómo funciona.


Entonces, agrega un estado DIV (inicialmente vacío) y cambia el controlador onClick (function LongCalc() ) para hacer 4 cosas:

  • Establezca el estado "Cálculo ... puede tardar 3 minutos" en el estado de DIV

  • Hace un cálculo muy largo (por ejemplo, toma 3 minutos)

  • Imprime los resultados del cálculo como resultado de la div.

  • Rellene el estado "El cálculo se completa" en el estado DIV

Y está feliz de tomar la aplicación para volver a probar.

Vuelven a ti muy enojados. ¡Y explique que cuando presionaron el botón, el estado DIV nunca se actualizó con el estado "Cálculo ..."!


Te rascas la cabeza, haces una pregunta sobre StackOverflow (o lees documentos o Google) y entiendes el problema:

border=0

El navegador coloca todas sus tareas TODO (tanto las tareas de la interfaz de usuario como los comandos de JavaScript) que resultan de los eventos en una sola cola . Y, desafortunadamente, volver a dibujar el "estado" DIV con el nuevo valor "Cálculo ..." es un TODO separado que se dirige al final de la cola.

Aquí está el desglose de eventos durante la prueba de usuario, el contenido de la cola después de cada evento:

  • Cola: [Empty]
  • Evento: Haga clic en el botón. La cola después del evento: [Execute OnClick handler(lines 1-4)]
  • Evento: ejecute la primera línea en el controlador OnClick (por ejemplo, cambie el valor de estado DIV). La cola después del evento: [Execute OnClick handler(lines 2-4), re-draw Status DIV with new "Calculating" value] . Tenga en cuenta que, aunque los cambios en DOM se producen de forma instantánea, para volver a dibujar el elemento DOM correspondiente, necesita un nuevo evento provocado por un cambio en DOM que se encontraba al final de la cola .
  • Problema !!! Problema !!! Los detalles se explican a continuación.
  • Evento: ejecutar la segunda línea en el controlador (cálculo). Cola después: [Execute OnClick handler(lines 3-4), re-draw Status DIV with "Calculating" value] .
  • Evento: ejecute la tercera línea en el controlador (complete el resultado DIV). Cola después: [Execute OnClick handler(line 4), re-draw Status DIV with "Calculating" value, re-draw result DIV with result] .
  • Evento: ejecute la cuarta línea en el controlador (ingrese el estado DIV con "DONE"). Cola: [Execute OnClick handler, re-draw Status DIV with "Calculating" value, re-draw result DIV with result; re-draw Status DIV with "DONE" value] [Execute OnClick handler, re-draw Status DIV with "Calculating" value, re-draw result DIV with result; re-draw Status DIV with "DONE" value] .
  • Evento: ejecutar un return implícito en el sub manejador onClick . Tomamos el "Execute OnClick handler" de la cola y comenzamos a ejecutar el siguiente elemento en la cola.
  • NOTA. Como ya hemos completado el cálculo, ya han pasado 3 minutos para el usuario. El evento de re-draw aún no ha ocurrido.
  • Evento: repita el dibujo del estado DIV utilizando el valor de Cálculo. Lo volvemos a dibujar y lo eliminamos de la cola.
  • Evento: repetir el dibujo del resultado del DIV con el resultado del resultado. Lo volvemos a dibujar y lo eliminamos de la cola.
  • Evento: repita el dibujo del estado de DIV con el valor "Hecho". Lo volvemos a dibujar y lo eliminamos de la cola. Es posible que los espectadores con ojos agudos noten un "DIV de estado con" Cálculo "intermitente durante una fracción de microsegundo: DESPUÉS DE QUE SE CUMPLE EL CÁLCULO

Por lo tanto, el problema principal es que el evento D para el re-dibujo "Estado" se coloca en una cola al final, DESPUÉS del evento "ejecutar línea 2", que dura 3 minutos, por lo que el dibujo repetido real no ocurrirá antes del cálculo POST.


setTimeout() viene al rescate. ¿Cómo ayuda esto? Porque, al llamar a un código ejecutable largo a través de setTimeout , realmente creas 2 eventos: la ejecución de setTimeout sí misma y (debido al tiempo de espera 0), entradas de cola separadas para el código ejecutable.

Entonces, para solucionar su problema, cambia el controlador onClick como una instrucción DOS (en una función nueva o solo en un bloque dentro de onClick ):

  • Establezca el estado "Cálculo ... puede tardar 3 minutos" en el estado de DIV

  • Ejecute setTimeout() con un tiempo de espera de 0 y llame a la función LongCalc() .

    LongCalc() función LongCalc() es casi la misma que la última vez, pero, obviamente, no tiene el estado "Cálculo ..." Actualización de DIV como primer paso; y en su lugar comienza el cálculo de inmediato.

Entonces, ¿cómo es la secuencia de eventos y la cola?

  • Cola: [Empty]
  • Evento: Haga clic en el botón. La cola después del evento: [Execute OnClick handler(status update, setTimeout() call)]
  • Evento: ejecute la primera línea en el controlador OnClick (por ejemplo, cambie el valor de estado DIV). La cola después del evento: [Execute OnClick handler(which is a setTimeout call), re-draw Status DIV with new "Calculating" value] .
  • Evento: ejecute la segunda línea en el controlador (llamada setTimeout). Queue after: [re-draw Status DIV with "Calculating" value] . No hay nada nuevo en la cola por otros 0 segundos.
  • Evento: la alarma de tiempo de espera se apaga, después de 0 segundos. Queue after: [re-draw Status DIV with "Calculating" value, execute LongCalc (lines 1-3)] .
  • Evento: volver a dibujar el estado DIV con el valor "Calculado" . La cola después de: [execute LongCalc (lines 1-3)] . Tenga en cuenta que este evento de re-dibujo puede ocurrir antes de que suene la alarma, lo que también funciona.
  • ...

¡Hurra! El estado DIV se acaba de actualizar a "Cálculo ..." antes de comenzar el cálculo.



A continuación se muestra un código de ejemplo de JSFiddle que ilustra estos ejemplos: http://jsfiddle.net/C2YBE/31/ :

Código HTML:

 <table border=1> <tr><td><button id='do'>Do long calc - bad status!</button></td> <td><div id='status'>Not Calculating yet.</div></td> </tr> <tr><td><button id='do_ok'>Do long calc - good status!</button></td> <td><div id='status_ok'>Not Calculating yet.</div></td> </tr> </table> 

Código JavaScript: ( onDomReady posible que se requiera Ejecutar en onDomReady y jQuery 1.9)

 function long_running(status_div) { var result = 0; // Use 1000/700/300 limits in Chrome, // 300/100/100 in IE8, // 1000/500/200 in FireFox // I have no idea why identical runtimes fail on diff browsers. for (var i = 0; i < 1000; i++) { for (var j = 0; j < 700; j++) { for (var k = 0; k < 300; k++) { result = result + i + j + k; } } } $(status_div).text('calculation done'); } // Assign events to buttons $('#do').on('click', function () { $('#status').text('calculating....'); long_running('#status'); }); $('#do_ok').on('click', function () { $('#status_ok').text('calculating....'); // This works on IE8. Works in Chrome // Does NOT work in FireFox 25 with timeout =0 or =1 // DOES work in FF if you change timeout from 0 to 500 window.setTimeout(function (){ long_running('#status_ok') }, 0); }); 
 function long_running(status_div) { var result = 0; // Use 1000/700/300 limits in Chrome, // 300/100/100 in IE8, // 1000/500/200 in FireFox // I have no idea why identical runtimes fail on diff browsers. for (var i = 0; i < 1000; i++) { for (var j = 0; j < 700; j++) { for (var k = 0; k < 300; k++) { result = result + i + j + k; } } } $(status_div).text('calculation done'); } // Assign events to buttons $('#do').on('click', function () { $('#status').text('calculating....'); long_running('#status'); }); $('#do_ok').on('click', function () { $('#status_ok').text('calculating....'); // This works on IE8. Works in Chrome // Does NOT work in FireFox 25 with timeout =0 or =1 // DOES work in FF if you change timeout from 0 to 500 window.setTimeout(function (){ long_running('#status_ok') }, 0); }); 
492
01 янв. La respuesta se da DVK 01 de enero. 2011-01-01 20:53 '11 a las 20:53 2011-01-01 20:53

Eche un vistazo al artículo de John Resig sobre Cómo funcionan los temporizadores de JavaScript . Cuando establece el tiempo de espera, en realidad puso en cola el código asíncrono hasta que ejecuta la pila de llamadas actual.

74
07 дек. Responder Andy 07 Dec 2010-12-07 00:38 '10 a las 0:38 2010-12-07 00:38

La mayoría de los navegadores tienen un proceso llamado hilo principal, que es responsable de realizar algunas tareas de JavaScript, actualizar la interfaz de usuario, por ejemplo, dibujar, volver a dibujar o reprogramar, etc.

Algunas tareas de actualización de JavaScript y la ejecución de la interfaz de usuario se ponen en cola en la cola de mensajes del navegador, y luego se envían a la cadena principal del navegador a ejecutar.

Cuando se generan actualizaciones de la interfaz de usuario, cuando el hilo principal está ocupado, las tareas se agregan a la cola de mensajes.

setTimeout(fn, 0); agregue este fn al final de la cola para ser ejecutado. Planea agregar una tarea a la cola de mensajes después de un período de tiempo específico.

21
26 февр. La respuesta es dada el 26 de febrero. 2013-02-26 00:27 '13 a las 0:27 2013-02-26 00:27

setTimeout() compra algo de tiempo hasta que se cargan los elementos DOM, incluso si se establece en 0.

Échale un vistazo: setTimeout

18
23 апр. Responder Jose Basilio 23 Abr 2009-04-23 00:52 '09 a las 0:52 2009-04-23 00:52

Aquí hay respuestas contradictorias, y sin evidencia no hay manera de saber en quién creer. Aquí está la prueba de que @DVK es correcto y @SalvadorDali está equivocado. Los últimos estados:

"Y aquí es por qué: es imposible establecer setTimeout con un retraso de 0 milisegundos. El valor mínimo está determinado por el navegador, y no es 0 milisegundos. Históricamente, los navegadores configuran esto en al menos 10 milisegundos, pero las especificaciones HTML5 y los navegadores modernos lo establecen en 4 milisegundos".

Un 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 demora unos segundos, la devolución de llamada no se ejecutará durante unos segundos hasta que se complete el código de bloqueo. Prueba este código:

 function testSettimeout0 () { var startTime = new Date().getTime() console.log('setting timeout 0 callback at ' +sinceStart()) setTimeout(function(){ console.log('in timeout callback at ' +sinceStart()) }, 0) console.log('starting blocking loop at ' +sinceStart()) while (sinceStart() < 3000) { continue } console.log('blocking loop ended at ' +sinceStart()) return // functions below function sinceStart () { return new Date().getTime() - startTime } // sinceStart } // testSettimeout0 

Salida:

 setting timeout 0 callback at 0 starting blocking loop at 5 blocking loop ended at 3000 in timeout callback at 3033 
15
26 июля '14 в 3:30 2014-07-26 03:30 La respuesta es dada por Vladimir Kornea el 26 de julio de 2014 a las 3:30 2014-07-26 03:30

Una de las razones para esto es retrasar la ejecución del código hasta un ciclo separado de eventos posteriores. Al responder a un evento del navegador (por ejemplo, un clic del mouse), a veces es necesario realizar operaciones solo después de procesar el evento actual. El objeto setTimeout() es la forma más fácil de hacer esto.

Ahora edito que en 2015 tengo que tener en cuenta que también hay un requestAnimationFrame() , que no es lo mismo, pero lo suficientemente cerca como para setTimeout(fn, 0) , que vale la pena mencionar.

12
01 янв. La respuesta se da puntiaguda 01 de enero. 2011-01-01 20:38 '11 a las 20:38 2011-01-01 20:38

Estas son viejas preguntas con viejas respuestas. Me gustaría agregar una nueva mirada a este problema y responder por qué sucede y no por qué es útil.

Entonces, tienes dos funciones:

 var f1 = function () { setTimeout(function(){ console.log("f1", "First function call..."); }, 0); }; var f2 = function () { console.log("f2", "Second call..."); }; 

y luego llámelos en el siguiente orden f1(); f2(); f1(); f2(); Para ver que el segundo corre primero.

Y es por esto que: es imposible tener setTimeout con un retraso de 0 milisegundos. El valor mínimo lo determina el navegador y no es igual a 0 milisegundos. Históricamente, el navegador establece este mínimo en 10 milisegundos, pero las especificaciones HTML5 y los navegadores modernos lo establecen en 4 milisegundos,

Si el nivel de anidamiento es más de 5 y el tiempo de espera es menos de 4, aumente el tiempo de espera a 4.

También desde mozilla:

Para realizar un tiempo de espera de 0 ms en un navegador moderno, puede usar window.postMessage (), como se describe aquí .

La información de PS se toma después de leer el siguiente artículo .

9
20 мая '14 в 0:34 2014-05-20 00:34 la respuesta la da Salvador Dalí el 20 de mayo de 2014 a las 0:34 2014-05-20 00:34

Dado que se pasa una longitud de 0 , creo que es necesario eliminar el código pasado a setTimeout del hilo. Por lo tanto, si esta es una función que puede tomar algún tiempo, no impedirá la ejecución del siguiente código.

8
01 янв. respuesta dada por user113716 01 de enero 2011-01-01 20:39 '11 a las 20:39 2011-01-01 20:39

Otra cosa es empujar una llamada de función en la parte inferior de la pila, evitando el desbordamiento de pila si se llama recursivamente a una función. Esto conduce a un while , pero permite que el mecanismo javascript inicie otros temporizadores asíncronos.

4
21 июня '12 в 4:14 2012-06-21 04:14 La respuesta la da Jason Suárez el 21 de junio de 2012 a las 4:14 am 2012-06-21 04:14

Las respuestas a la ejecución de bucles y la representación de DOM antes de ejecutar algunos otros códigos son correctas. Cero tiempos de espera secundarios en JavaScript ayudan a hacer que el código sea pseudo-multihilo, aunque este no es el caso.

Quiero agregar que el MEJOR valor para la latencia cero entre navegadores / plataformas cruzadas en JavaScript es en realidad de unos 20 milisegundos en lugar de 0 (cero), ya que muchos navegadores móviles no pueden registrar tiempos de espera de menos de 20 milisegundos para restringir horas en chips AMD.

Además, los procesos de larga ejecución que no están relacionados con la manipulación del DOM ahora deben enviarse a los trabajadores web, ya que proporcionan una verdadera ejecución de múltiples subprocesos de JavaScript.

2
14 марта '13 в 2:29 2013-03-14 02:29 La respuesta la da ChrisN el 14 de marzo de 2013 a las 2:29. 2013-03-14 02:29

Al llamar a setTimeout, especifica el tiempo de la página para responder a lo que está haciendo el usuario. Esto es especialmente útil para las funciones realizadas durante la carga de la página.

1
23 апр. Respuesta dada por Jeremy el 23 de abril. 2009-04-23 00:53 '09 a las 0:53 2009-04-23 00:53

Algunos otros casos en los que setTimeout es útil:

Desea romper un ciclo largo o calcularlo en componentes más pequeños para que el navegador no parezca "congelado" o decir que "El script en la página está ocupado".

Desea deshabilitar el botón de envío cuando hace clic, pero si deshabilita el botón en el controlador onClick, no se enviará el formulario. Un valor cero setTimeout hace el truco de dejar que el evento finalice, el formulario para comenzar a enviar, luego su botón puede ser deshabilitado.

1
14 дек. La respuesta se da fabspro 14 dec. 2012-12-14 16:09 '12 a las 4:09 PM 2012-12-14 16:09

setTimout en 0 también es muy útil en una plantilla de configuración de promesa pendiente que desea devolver de inmediato:

 myObject.prototype.myMethodDeferred = function() { var deferredObject = $.Deferred(); var that = this; // Because setTimeout won't work right with this setTimeout(function() { return myMethodActualWork.call(that, deferredObject); }, 0); return deferredObject.promise(); } 
1
02 сент. La respuesta es dada por Stephan G 02 Sep. 2015-09-02 18:39 '15 a las 18:39 2015-09-02 18:39

Otras preguntas sobre etiquetas o hacer una pregunta