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


872

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 selectedIndexno 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 selectedIndexse 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()?


2
La mayor parte de la pregunta describe por qué es útil. Si necesita saber por qué sucede esto, lea mi respuesta: stackoverflow.com/a/23747597/1090562
Salvador Dali

18
Philip Roberts explica esto de la mejor manera posible aquí en su charla "¿Qué diablos es el ciclo de eventos?" youtube.com/watch?v=8aGhZQkoFbQ
vasa

Si tiene prisa, esta es la parte del video que comienza a abordar la pregunta exactamente: youtu.be/8aGhZQkoFbQ?t=14m54s . Sin duda, vale la pena ver todo el video.
William Hampshire

44
setTimeout(fn)es igual que setTimeout(fn, 0), por cierto.
vsync

Respuestas:


827

En la pregunta, existía una condición de carrera entre:

  1. El intento del navegador de inicializar la lista desplegable, listo para actualizar su índice seleccionado, y
  2. Su código para establecer el índice seleccionado

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:

setTimeout(callback, 0)

Invocar setTimeoutcon 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.


276
"La solución es" pausar "la ejecución de JavaScript para permitir que los hilos de renderizado se pongan al día". No del todo cierto, lo que hace setTimeout es agregar un nuevo evento a la cola de eventos del navegador y el motor de representación ya está en esa cola (no del todo cierto, pero lo suficientemente cerca) para que se ejecute antes del evento setTimeout.
David Mulder

48
Sí, esa es una respuesta mucho más detallada y mucho más correcta. Pero el mío es "lo suficientemente correcto" para que la gente entienda por qué funciona el truco.
staticsan

2
@DavidMulder, ¿significa que el navegador analiza CSS y renderiza en un hilo diferente del hilo de ejecución de JavaScript?
Jason

8
No, en principio se analizan en el mismo hilo, de lo contrario, algunas líneas de manipulación DOM desencadenarían reflujos todo el tiempo, lo que tendría una influencia extremadamente mala en la velocidad de ejecución.
David Mulder

30
Este video es la mejor explicación de por qué establecemos TimeTime 0 2014.jsconf.eu/speakers/…
davibq

665

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 onClickcontrolador 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, toma 3 min)

  2. 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 onclickcontrolador (función LongCalc()) para hacer 4 cosas:

  1. Rellene el estado "Calculando ... puede tardar ~ 3 minutos" en el estado DIV

  2. Hace un cálculo muy largo (por ejemplo, toma 3 min)

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

  4. 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:

  • Cola: [Empty]
  • Evento: haga clic en el botón. Cola después del evento:[Execute OnClick handler(lines 1-4)]
  • Evento: ejecute la primera línea en el controlador OnClick (p. Ej., Cambie el valor del estado DIV). 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 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 .
  • ¡¡¡PROBLEMA!!! ¡¡¡PROBLEMA!!! Detalles explicados 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 (llenar 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 (complete 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 implícitamente returndesde el onclickcontrolador sub. Quitamos el "Ejecutar controlador de OnClick" de la cola y comenzamos a ejecutar el siguiente elemento en la cola.
  • NOTA: Como ya terminamos el cálculo, ya pasaron 3 minutos para el usuario. ¡El evento de re-sorteo aún no sucedió!
  • Evento: volver a dibujar el estado DIV con el valor "Cálculo". Hacemos el sorteo y lo sacamos de la cola.
  • Evento: volver a dibujar el resultado DIV con el valor del resultado. Hacemos el sorteo y lo sacamos de la cola.
  • Evento: vuelva a dibujar el estado DIV con el valor "Listo". Hacemos el sorteo y lo sacamos de la cola. Los espectadores con ojos agudos incluso podrían notar que el "Estado DIV con el valor" Cálculo "parpadea por una fracción de microsegundo - DESPUÉS DE QUE EL CÁLCULO TERMINÓ

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 prolongada setTimeout, en realidad crea 2 eventos: la setTimeoutejecució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 onClickcontrolador para que sea DOS declaraciones (en una nueva función o simplemente un bloque dentro onClick):

  1. Rellene el estado "Calculando ... puede tardar ~ 3 minutos" en el estado DIV

  2. Ejecutar setTimeout()con 0 tiempo de espera y una llamada a la LongCalc()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?

  • Cola: [Empty]
  • Evento: haga clic en el botón. Cola después del evento:[Execute OnClick handler(status update, setTimeout() call)]
  • Evento: ejecute la primera línea en el controlador OnClick (p. Ej., Cambie el valor del estado DIV). Cola después del evento: [Execute OnClick handler(which is a setTimeout call), re-draw Status DIV with new "Calculating" value].
  • Evento: ejecutar 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: se activa la alarma del tiempo de espera, 0 segundos después. Cola después: [re-draw Status DIV with "Calculating" value, execute LongCalc (lines 1-3)].
  • Evento: volver a dibujar el estado DIV con el valor "Cálculo" . Cola después: [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:

<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 onDomReadyy 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);
});

12
gran respuesta DVK! Aquí hay un resumen que ilustra su ejemplo gist.github.com/kumikoda/5552511#file-timeout-html
kumikoda

44
Muy buena respuesta, DVK. Solo para que sea fácil de imaginar, he puesto ese código en jsfiddle jsfiddle.net/thangchung/LVAaV
thangchung

44
@ThangChung - Traté de hacer una mejor versión (2 botones, uno para cada caso) en JSFiddle. Funciona como una demostración en Chrome e IE, pero no en FF por alguna razón; consulte jsfiddle.net/C2YBE/31 . Pregunté por qué FF no funciona aquí: stackoverflow.com/questions/20747591/…
DVK

1
@DVK "El navegador coloca todas sus tareas" TODO "(tareas de IU y comandos JavaScript) resultantes de eventos en una sola cola". Señor, ¿puede proporcionar la fuente para esto? En mi humilde opinión enviaban exploradores supone que tienen interfaz de usuario diferente (motor de renderizado) y los hilos JS .... sin ánimo de ofender .... simplemente quieren aprender ..
bhavya_w

1
@bhavya_w No, todo sucede en un hilo. Esta es la razón por la que un cálculo largo de js puede bloquear la interfaz de usuario
Seba Kerckhof


23

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

Mira esto: setTimeout


23

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 esto fnal 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.


"Cada ejecución de JavaScript y las tareas de actualización de la interfaz de usuario se agregan al sistema de cola de eventos del navegador, luego esas tareas se envían al subproceso de interfaz de usuario principal del navegador para que se realice" ... ¿fuente por favor?
bhavya_w

44
JavaScript de alto rendimiento (Nicholas Zakas, Stoyan Stefanov, Ross Harmes, Julien Lecomte y Matt Sweeney)
Arley

Voto negativo para esto add this fn to the end of the queue . Lo más importante es dónde setTimeoutagrega exactamente esta función, el final de este ciclo de ciclo o el comienzo del siguiente ciclo de ciclo.
Verde

19

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:

"Y esta es la razón: no es posible establecer Timeout con un retraso de tiempo de 0 milisegundos. El valor mínimo lo determina el navegador y no es de 0 milisegundos. Históricamente los navegadores establecen este mínimo en 10 milisegundos, pero las especificaciones HTML5 y los navegadores modernos lo tienen 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

Salida es:

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

Tu respuesta no prueba nada. Simplemente muestra que en su máquina, en una situación específica, la computadora le arroja algunos números. Para probar algo relacionado, necesita un poco más que unas pocas líneas de código y unos pocos números.
Salvador Dali

66
@SalvadorDali, creo que mi prueba es lo suficientemente clara para que la mayoría de la gente la entienda. Creo que te sientes a la defensiva y no has hecho el esfuerzo de entenderlo. Estaré encantado de intentar aclararlo, pero no sé qué es lo que no entiendes. Intente ejecutar el código en su propia máquina si sospecha de mis resultados.
Vladimir Kornea

Trataré de aclararlo por última vez. Mi respuesta es aclarar algunas de las propiedades interesantes en tiempo de espera, intervalo y está bien respaldado por fuentes válidas y reconocibles. Hiciste una fuerte afirmación de que está mal y apuntaste a tu código que por alguna razón nombraste una prueba. No hay nada malo con su código (no estoy en contra de él). Solo digo que mi respuesta no es incorrecta. Aclara algunos puntos. Voy a detener esta guerra, porque no puedo ver un punto.
Salvador Dali

15

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 para setTimeout(fn, 0)que valga la pena mencionarlo.


Este es precisamente uno de los lugares donde he visto que se usa. =)
Zaibot

FYI: esta respuesta se fusionó aquí desde stackoverflow.com/questions/4574940/…
Shog9

2
es diferir la ejecución de código a un bucle de eventos posterior separado : ¿cómo se calcula un bucle de eventos posterior ? ¿Cómo averiguar qué es un bucle de eventos actual? ¿Cómo sabes en qué giro de evento estás ahora?
Verde

@ Green bien, en realidad no; realmente no hay visibilidad directa de lo que está haciendo el tiempo de ejecución de JavaScript.
Puntiagudo

requestAnimationFrame resolvió el problema que tenía con IE y Firefox no actualizaba la interfaz de usuario a veces
David

9

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:

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

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

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 setTimeoutun 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.

Si el nivel de anidación es superior a 5 y el tiempo de espera es inferior a 4, aumente 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í .

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


1
@ user2407309 ¿Estás bromeando? ¿Quieres decir que la especificación HTML5 es incorrecta y estás en lo correcto? Lea las fuentes antes de votar y haga afirmaciones contundentes. Mi respuesta se basa en la especificación HTML y el registro histórico. En lugar de hacer la respuesta que explica completamente las mismas cosas una y otra vez, agregué algo nuevo, algo que no se mostró en las respuestas anteriores. No estoy diciendo que esta sea la única razón, solo estoy mostrando algo nuevo.
Salvador Dali

1
Esto es incorrecto: "Y esta es la razón: no es posible establecer setTimeout con un retraso de tiempo de 0 milisegundos". Eso no es por qué. El retraso de 4 ms es irrelevante por qué setTimeout(fn,0)es útil.
Vladimir Kornea

@ user2407309 se puede modificar fácilmente a "para agregar a las razones expuestas por otros, no es posible ...". Por lo tanto, es ridículo hacer un voto negativo solo por esto, especialmente si su propia respuesta no dice nada nuevo. Solo una pequeña edición sería suficiente.
Salvador Dali

10
Salvador Dali: Si ignoras los aspectos emocionales de la guerra de las micro llamas aquí, probablemente tengas que admitir que @VladimirKornea tiene razón. Es cierto que los navegadores asignan un retraso de 0 ms a 4 ms, pero incluso si no lo hicieran, los resultados seguirían siendo los mismos. El mecanismo de conducción aquí es que el código se inserta en la cola, en lugar de la pila de llamadas. Eche un vistazo a esta excelente presentación de JSConf, puede ayudar a aclarar el problema: youtube.com/watch?v=8aGhZQkoFbQ
hashchange

1
No sé por qué cree que su presupuesto para un mínimo calificado de 4 ms es un mínimo global de 4 ms. A medida que su cita de espectáculos de la especificación HTML5, el mínimo es de 4 ms solamente cuando usted tiene llamadas anidadas a setTimeout/ setIntervalmá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, setIntervalcon un intervalo de 0 se reprogramará inmediatamente varias veces, luego se retrasará más después de eso), pero los usos simples de setTimeoutanidación mínima se pueden poner en cola de inmediato.
ShadowRanger

8

Dado que se pasa una duración de 0, supongo que es para eliminar el código pasado setTimeoutdel 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.


FYI: esta respuesta se fusionó aquí desde stackoverflow.com/questions/4574940/…
Shog9

7

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 clickeventos al evento en el #dobotón.

Luego, cuando hace clic en el botón, messagese crea una referencia a la función del controlador de eventos, que se agrega a la message queue. Cuando event loopllega a este mensaje, crea un frameen 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 onfunció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 setTimeoutcosa 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 windowcontexto, 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 0no "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 setTimeouty cuál es una mejor solución para este caso en particular?

En primer lugar, el problema con el uso setTimeoutde 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 0para 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.


3

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 whilebucle, pero permite que el motor de JavaScript active otros temporizadores asíncronos.


Voto negativo para esto push the function invocation to the bottom of the stack. De qué stackestás hablando es oscuro. Lo más importante es dónde setTimeoutagrega exactamente esta función, el final de este ciclo de ciclo o el comienzo del siguiente ciclo de ciclo.
Verde

1

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.


1

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.


2
La desactivación se haría mejor en el evento de envío; sería más rápido y se garantiza que se llamará antes de que el formulario se envíe técnicamente, ya que puede detener el envío.
Kris

Muy cierto. Supongo que deshabilitar onclick es más fácil para la creación de prototipos porque simplemente puede escribir onclick = "this.disabled = true" en el botón, mientras que deshabilitar el envío requiere un poco más de trabajo.
fabspro

1

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.


2
Soy un poco escéptico acerca de su respuesta, pero la voté porque me obligó a investigar un poco más sobre los estándares del navegador. Cuando investigo estándares, voy a donde siempre voy, MDN: developer.mozilla.org/en-US/docs/Web/API/window.setTimeout La especificación HTML5 dice 4ms. No dice nada sobre las limitaciones de reloj en chips móviles. Tener dificultades para buscar en Google una fuente de información para respaldar sus declaraciones. Descubrí que Dart Language by Google eliminó setTimeout por completo a favor de un objeto Timer.
John Zabroski

8
(...) debido a que muchos navegadores móviles no pueden registrar los tiempos de espera de menos de 20 milisegundos debido a las limitaciones de reloj (...) Cada plataforma tiene limitaciones de tiempo debido a su reloj y ninguna plataforma es capaz de ejecutar la siguiente cosa exactamente 0 ms después de la el actual. El tiempo de espera de 0 ms solicita la ejecución de una función lo antes posible y las limitaciones de tiempo de una plataforma específica no cambian el significado de esto de ninguna manera.
Piotr Dobrogost

1

setTimout en 0 también es muy útil en el patrón de configuración de una promesa diferida, 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

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:

  1. 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 llamada setTimeout()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.
  2. El valor 0 para ms como segundo argumento en la función 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 las setTimeout()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.

-1

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

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.