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


Recientemente me encontré con un error bastante desagradable, en el que el código se estaba cargando <select> dinámicamente a través de JavaScript. Esto cargado dinámicamente <select> tenía un valor preseleccionado En IE6, ya teníamos código para corregir el seleccionado <option>, porque a veces el <select>es selectedIndex el valor no estaría sincronizado con el seleccionado <option>es index atributo, como a continuación:

field.selectedIndex = element.index;

Sin embargo, este código no funcionaba. Aunque el campo selectedIndex se estaba configurando correctamente, el índice incorrecto terminaría siendo seleccionado. Sin embargo, si metí un alert() declaración en el momento adecuado, se seleccionará la opción correcta. Pensando que esto podría ser algún tipo de problema de tiempo, intenté 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 inquieta que no sé 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" usando setTimeout()?


703
2018-04-22 21:46


origen


Respuestas:


Esto funciona porque estás haciendo múltiples tareas cooperativas.

Un navegador tiene que hacer varias cosas al mismo tiempo, y solo una de ellas es ejecutar JavaScript. Pero una de las cosas con las que JavaScript se usa con frecuencia es pedirle al navegador que cree un elemento de visualización. Esto a menudo se supone que se realiza de forma síncrona (particularmente porque JavaScript no se ejecuta en paralelo), pero no hay garantía de que este sea el caso y JavaScript no tiene un mecanismo bien definido para esperar.

La solución es "pausar" la ejecución de JavaScript para permitir que los hilos de representación se pongan al día. Y este es el efecto que setTimeout() con un tiempo de espera de 0 hace. Es como un rendimiento de hilo / proceso en C. Aunque parece decir "ejecutar esto inmediatamente", en realidad le da al navegador la oportunidad de terminar haciendo cosas que no son de JavaScript y que han estado esperando terminar antes de atender a esta nueva pieza de JavaScript. .

(En la actualidad, setTimeout() vuelve a poner en cola el nuevo JavaScript al final de la cola de ejecución. Vea los comentarios para enlaces a una explicación más larga.)

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


666
2018-04-23 00:14



Prefacio:

NOTA IMPORTANTE: Si bien se votó con más votos positivos y aceptados, la respuesta aceptada por @staticsan en realidad es ¡INCORRECTO! - ver el comentario de David Mulder para explicar por qué.

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 se usa setTimeout() ayuda. Parece bastante largo, pero en realidad es muy simple y directo, lo hice muy detallado.

ACTUALIZAR: Hice un JSFiddle para vivir; demuestro la siguiente explicación: http://jsfiddle.net/C2YBE/31/ . Muchos Gracias a @ThangChung por ayudar a ponerlo en marcha.

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


DETALLES:

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

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

  1. Hace un cálculo muy largo (por ejemplo, tarda 3 minutos)

  2. Imprime los resultados del cálculo en el resultado div.

Ahora, los 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 vuelven inquietos, vuelven a hacer clic, esperan 1 minuto, no pasa nada, vuelven a hacer clic en el botón ...

El problema es obvio: quiere un DIV de "estado", que muestra lo que está sucediendo. Veamos cómo funciona eso.


Entonces agrega un DIV "Status" (inicialmente vacío) y modifica el onclick controlador (función LongCalc()) para hacer 4 cosas:

  1. Complete el estado "Calcular ... puede tomar ~ 3 minutos" en el estado DIV

  2. Hace un cálculo muy largo (por ejemplo, tarda 3 minutos)

  3. Imprime los resultados del cálculo en el resultado div.

  4. Complete el estado "Cálculo realizado" en el estado DIV

Y, felizmente le das la aplicación a los usuarios para volver a probarla.

Vuelven a verte muy enojados. Y explica que cuando hicieron clic en el botón, el Status DIV nunca se actualizó con el estado "Cálculo ..."


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" (ambas tareas de UI y comandos de JavaScript) resultantes de eventos en un cola única. Y desafortunadamente, volver a dibujar el DIV "Status" con el nuevo valor "Calculating ..." es un TODO separado que va al final de la cola.

Aquí hay un desglose de los eventos durante la prueba de su usuario, los contenidos de la cola después de cada evento:

  • Cola: [Empty]
  • Evento: haga clic en el botón. Cola después del evento: [Execute OnClick handler(lines 1-4)]
  • Evento: ejecuta la primera línea en el controlador OnClick (por ejemplo, cambia el valor de DIV de estado). Cola después del evento: [Execute OnClick handler(lines 2-4), re-draw Status DIV with new "Calculating" value]. Tenga en cuenta que, si bien los cambios 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..
  • ¡¡¡PROBLEMA!!!  ¡¡¡PROBLEMA!!! Detalles explicados a continuación.
  • Evento: ejecuta 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 3ra línea en el controlador (llene 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 4ª línea en el controlador (llene el estado DIV con "HECHO"). Cola: [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 implicado return de onclick controlador sub. Tomamos el "Controlador de ejecución de OnClick" fuera de la cola y comenzamos a ejecutar el siguiente elemento en la cola.
  • NOTA: Como ya hemos terminado el cálculo, ya pasaron 3 minutos para el usuario. El evento de volver a dibujar aún no había sucedido!
  • Evento: vuelva a dibujar Status DIV con el valor de "Cálculo". Hacemos el redibujado y quitamos eso de la cola.
  • Evento: vuelva a dibujar el resultado DIV con el valor del resultado. Hacemos el redibujado y quitamos eso de la cola.
  • Evento: vuelva a dibujar Status DIV con el valor "Hecho". Hacemos el redibujado y quitamos eso de la cola. Los espectadores de ojos agudos incluso pueden notar "Status DIV con" Calculating "value flashing por fracción de microsegundo - DESPUÉS DE QUE EL CÁLCULO HAYA TERMINADO

Entonces, el problema subyacente es que el evento de volver a dibujar para el DIV "Estado" se coloca en la cola al final, DESPUÉS del evento "ejecutar línea 2", que dura 3 minutos, por lo que el realineamiento real no ocurre hasta DESPUÉS de que el cálculo esté hecho.


Al rescate viene el setTimeout(). ¿Cómo ayuda? Porque llamando al código de ejecución larga a través de setTimeout, realmente creas 2 eventos: setTimeout ejecución propiamente dicha, y (debido al tiempo de espera 0), entrada de cola separada para el código que se está ejecutando.

Entonces, para solucionar su problema, usted modifica su onClick controlador para ser DOS instrucciones (en una nueva función o solo un bloque dentro onClick)

  1. Complete el estado "Calcular ... puede tomar ~ 3 minutos" en el estado DIV

  2. Ejecutar setTimeout() con 0 tiempo de espera y una llamada a LongCalc() función.

    LongCalc() la función es casi la misma que la última vez, pero obviamente no tiene la actualización de DIV de estado "Cálculo ..." 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?

  • Cola: [Empty]
  • Evento: haga clic en el botón. Cola después del evento: [Execute OnClick handler(status update, setTimeout() call)]
  • Evento: ejecuta la primera línea en el controlador OnClick (por ejemplo, cambia el valor de DIV de estado). Cola después del evento: [Execute OnClick handler(which is a setTimeout call), re-draw Status DIV with new "Calculating" value].
  • Evento: ejecuta la segunda línea en el controlador (llamada setTimeout). Cola después: [re-draw Status DIV with "Calculating" value]. La cola no tiene nada nuevo durante 0 segundos más.
  • Evento: la alarma del tiempo de espera se apaga, 0 segundos después. Cola después: [re-draw Status DIV with "Calculating" value, execute LongCalc (lines 1-3)].
  • Evento: volver a dibujar Status DIV con el valor de "Cálculo". Cola después: [execute LongCalc (lines 1-3)]. Tenga en cuenta que este evento de volver a dibujar podría suceder ANTES de que se active la alarma, que funciona igual de bien.
  • ...

¡Hurra! El Status DIV acaba de actualizarse a "Cálculo ..." antes de comenzar 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:

<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: (Ejecutado en onDomReady y puede requerir 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);
});

558
2018-01-01 17:53



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 asíncrono hasta que el motor ejecuta la pila de llamadas actual.


82
2017-12-06 21:38



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, pintura, redibujado o reflujo, etc.

Algunas tareas de ejecución de JavaScript y de actualización de la interfaz de usuario se colocan en la cola de mensajes del navegador y luego se envían al hilo principal del navegador para que se ejecuten.

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

setTimeout(fn, 0); Agrega esto fn hasta el final de la cola para ser ejecutado. Programa una tarea que se agregará en la cola de mensajes después de un período de tiempo determinado.


20
2018-04-22 21:52



setTimeout() le compra algo de tiempo hasta que se carguen los elementos DOM, incluso si está configurado en 0.

Mira esto: setTimeout


19
2017-07-26 00:30



Aquí hay respuestas contrapuestas conflictivas, y sin pruebas no hay forma de saber a quién creer. Aquí hay una prueba de que @DVK es correcto y @SalvadorDali está equivocado. El último reclama:

"Y aquí está el porqué: no es posible tener setTimeout con un tiempo   retraso de 0 milisegundos. El valor mínimo está determinado por   navegador y no es 0 milisegundos. Históricamente los navegadores establecen esto   mínimo a 10 milisegundos, pero las especificaciones de HTML5 y los navegadores modernos   tenerlo configurado en 4 milisegundos ".

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:

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

La salida es:

setting timeout 0 callback at 0
starting blocking loop at 5
blocking loop ended at 3000
in timeout callback at 3033

15
2018-01-01 17:38



Una razón para hacerlo es diferir la ejecución del código a un ciclo de eventos posterior e independiente. Al responder a un evento de navegador de algún tipo (clic del mouse, por ejemplo), a veces es necesario realizar operaciones solo después el evento actual es procesado. los setTimeout() la instalación es la forma más sencilla de hacerlo.

editar ahora que es 2015, debo señalar que también hay requestAnimationFrame(), que no es exactamente lo mismo pero es lo suficientemente cerca setTimeout(fn, 0) que vale la pena mencionar


12
2018-05-19 21:34



Esta es una pregunta antigua con respuestas antiguas. 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:

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

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

y luego llamarlos en el siguiente orden f1(); f2(); solo para ver que el segundo se ejecutó primero.

Y he aquí por qué: no es posible tener setTimeout con un retraso de tiempo de 0 milisegundos. los El valor mínimo es determinado por el navegador y no es 0 milisegundos. Históricamente navegadores establece este mínimo a 10 milisegundos, pero el Especificaciones de HTML5 y los navegadores modernos lo tienen configurado en 4 milisegundos.

Si el nivel de anidamiento es mayor que 5, y el tiempo de espera es menor que 4, entonces   aumentar el tiempo de espera a 4.

También de mozilla:

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

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


9
2018-01-01 17:39



Dado que se está aprobando una duración de 0, Supongo que es para eliminar el código pasado al setTimeout del flujo de ejecución. Entonces, si se trata de una función que puede llevar un tiempo, no impedirá la ejecución del siguiente código.


8
2018-06-21 01:14