Temporizadores de JavaScript: todo lo que necesitas saber

Hola colegas Había una vez en Habré un artículo escrito bajo la autoría de John Rezig solo sobre este tema. Han pasado 10 años y el tema aún requiere aclaración. Por lo tanto, ofrecemos a los interesados ​​leer el artículo de Samer Buna, que ofrece no solo una descripción teórica de los temporizadores en JavaScript (en el contexto de Node.js), sino también tareas sobre ellos.




Hace unas semanas tuiteé la siguiente pregunta de una sola entrevista:

“¿Dónde está el código fuente para las funciones setTimeout y setInterval? ¿Dónde lo buscarías? No puedes googlearlo :) "

*** Responde por ti mismo y luego sigue leyendo ***



Alrededor de la mitad de las respuestas a este tweet fueron incorrectas. ¡No, el caso NO ESTÁ RELACIONADO con V8 (u otras máquinas virtuales)! Funciones como setTimeout y setInterval , orgullosamente llamados JavaScript JavaScript Timers, no son parte de ninguna especificación ECMAScript o implementación de motor JavaScript. Las funciones del temporizador se implementan a nivel del navegador, por lo que su implementación difiere en los diferentes navegadores. Los temporizadores también se implementan de forma nativa en el tiempo de ejecución de Node.js.

En los navegadores, las funciones principales del temporizador se refieren a la interfaz de Window , que también está asociada con algunas otras funciones y objetos. Esta interfaz proporciona acceso global a todos sus elementos en el ámbito principal de JavaScript. Es por eso que la función setTimeout se puede ejecutar directamente en la consola del navegador.

En Node, los temporizadores son parte del objeto global , que está diseñado como la interfaz del navegador de Window . El código fuente de los temporizadores en Node se muestra aquí .

Puede parecerle a alguien que esta es solo una mala pregunta de la entrevista: ¿de qué sirve saber esto? Yo, como desarrollador de JavaScript, pienso de esta manera: se supone que debe saber esto, ya que lo contrario puede indicar que no comprende del todo cómo V8 (y otras máquinas virtuales) interactúan con los navegadores y Node.

Veamos algunos ejemplos y resuelva un par de tareas de temporizador, ¿vamos?

Puede usar el comando de nodo para ejecutar los ejemplos en este artículo. La mayoría de los ejemplos discutidos aquí aparecen en mi curso de Introducción a Node.js en Pluralsight.

Ejecución diferida de funciones

Los temporizadores son funciones de orden superior con las que puede retrasar o repetir la ejecución de otras funciones (el temporizador recibe una función como el primer argumento).

Aquí hay un ejemplo de ejecución diferida:

 // example1.js setTimeout( () => { console.log('Hello after 4 seconds'); }, 4 * 1000 ); 

En este ejemplo, usando setTimeout el mensaje de saludo se retrasa 4 segundos. El segundo argumento para setTimeout es el retraso (en ms). Multiplico 4 por 1000 para obtener 4 segundos.

El primer argumento para setTimeout es una función cuya ejecución se retrasará.
Si ejecuta el archivo example1.js con el comando de nodo, Node hará una pausa durante 4 segundos y luego mostrará un mensaje de bienvenida (seguido de una salida).

Tenga en cuenta: el primer argumento para setTimeout es solo una referencia de función . No debería ser una función incorporada, como example1.js . Aquí está el mismo ejemplo sin usar la función incorporada:

 const func = () => { console.log('Hello after 4 seconds'); }; setTimeout(func, 4 * 1000); 

Pasando argumentos

Si la función para la cual se usa setTimeout para retrasar acepta cualquier argumento, entonces puede usar los argumentos restantes de la función setTimeout sí (después de los 2 que ya hemos estudiado) para transferir los valores de los argumentos a la función diferida.

 // : func(arg1, arg2, arg3, ...) //  : setTimeout(func, delay, arg1, arg2, arg3, ...) 

Aquí hay un ejemplo:

 // example2.js const rocks = who => { console.log(who + ' rocks'); }; setTimeout(rocks, 2 * 1000, 'Node.js'); 

La función de rocks anterior, retrasada por 2 segundos, toma el argumento who , y al llamar a setTimeout pasa el valor "Node.js" como tal argumento who .

Al ejecutar example2.js con el comando de node , se mostrará la frase "Node.js rocks" después de 2 segundos.

Temporizadores Tarea # 1

Entonces, según el material ya estudiado sobre setTimeout , setTimeout los 2 mensajes siguientes después de los retrasos correspondientes.

  • El mensaje "Hola después de 4 segundos" se muestra después de 4 segundos.
  • El mensaje "Hola después de 8 segundos" se muestra después de 8 segundos.

Limitación

En su solución, puede definir solo una función que contenga funciones integradas. Esto significa que muchas llamadas setTimeout tendrán que usar la misma función.

Solución

Así es como resolvería este problema:

 // solution1.js const theOneFunc = delay => { console.log('Hello after ' + delay + ' seconds'); }; setTimeout(theOneFunc, 4 * 1000, 4); setTimeout(theOneFunc, 8 * 1000, 8); 

Para mí, theOneFunc recibe el argumento de delay y usa el valor de este argumento de delay en el mensaje que se muestra en la pantalla. Por lo tanto, la función puede mostrar diferentes mensajes dependiendo de qué valor de retraso le informaremos.

Luego utilicé el theOneFunc en dos llamadas setTimeout , la primera llamada se setTimeout después de 4 segundos y la segunda después de 8 segundos. Ambas llamadas setTimeout también reciben un tercer argumento, que representa el argumento de delay para el theOneFunc .

Al ejecutar el archivo solution1.js con el comando de nodo, mostraremos los requisitos de la tarea, y el primer mensaje aparecerá después de 4 segundos y el segundo después de 8 segundos.

Repite la función

Pero, ¿qué pasa si le pido que muestre un mensaje cada 4 segundos, por tiempo ilimitado?
Por supuesto, puede setTimeout en un bucle, pero la API del temporizador también ofrece la función setInterval , con la que puede programar la ejecución "eterna" de cualquier operación.

Aquí hay un ejemplo de setInterval :

 // example3.js setInterval( () => console.log('Hello every 3 seconds'), 3000 ); 

Este código mostrará un mensaje cada 3 segundos. Si ejecuta example3.js con el comando de node , Node generará este comando hasta que fuerce el final del proceso (CTRL + C).

Cancelar temporizadores

Como se asigna una acción cuando se llama a la función del temporizador, esta acción también se puede deshacer antes de ejecutarse.

La llamada setTimeout devuelve una ID de temporizador, y puede usar esta ID de temporizador cuando llame a clearTimeout para cancelar el temporizador. Aquí hay un ejemplo:

 // example4.js const timerId = setTimeout( () => console.log('You will not see this one!'), 0 ); clearTimeout(timerId); 

Este temporizador simple debería activarse después de 0 ms (es decir, inmediatamente), pero esto no sucederá, ya que capturamos el valor de timerId y cancelamos inmediatamente este temporizador llamando a clearTimeout .

Al ejecutar example4.js con el comando de node , Node no imprimirá nada; el proceso simplemente finalizará de inmediato.

Por cierto, Node.js también proporciona otra forma de establecer setTimeout con un valor de 0 ms. Hay otra función en la API del temporizador Node.js llamada setImmediate , y básicamente hace lo mismo que setTimeout con un valor de 0 ms, pero en este caso puede omitir el retraso:

 setImmediate( () => console.log('I am equivalent to setTimeout with 0 ms'), ); 

La función setImmediate es compatible con todos los navegadores . No lo use en el código del cliente.

Junto con clearTimeout hay una función clearInterval que hace lo mismo, pero con llamadas setInerval , y también hay una llamada clearImmediate .

Retardo del temporizador: algo no garantizado

¿Ha notado que en el ejemplo anterior, al realizar una operación con setTimeout después de 0 ms, esta operación no ocurre inmediatamente (después de setTimeout ), sino solo después de que todo el código del script se haya ejecutado por completo (incluida la llamada clearTimeout )?

Permítanme aclarar este punto con un ejemplo. Aquí hay una llamada simple setTimeout que debería funcionar en medio segundo, pero esto no sucede:

 // example5.js setTimeout( () => console.log('Hello after 0.5 seconds. MAYBE!'), 500, ); for (let i = 0; i < 1e10; i++) { //    } 

Inmediatamente después de definir el temporizador en este ejemplo, bloqueamos sincrónicamente el entorno de tiempo de ejecución con un bucle for grande. El valor de 1e10 es 1 con 10 ceros, por lo que el ciclo dura 10 mil millones de ciclos de procesador (en principio, esto simula un procesador sobrecargado). El nodo no puede hacer nada hasta que este ciclo se complete.

Por supuesto, en la práctica esto es muy malo, pero este ejemplo ayuda a comprender que el retraso setTimeout no está garantizado, sino el valor mínimo . Un valor de 500 ms significa que el retraso durará al menos 500 ms. De hecho, el script tardará mucho más en mostrar la línea de bienvenida en la pantalla. Primero, tendrá que esperar hasta que se complete el ciclo de bloqueo.

Temporizadores Problema # 2

Escriba un script que muestre el mensaje "Hola mundo" una vez por segundo, pero solo 5 veces. Después de 5 iteraciones, el script debe mostrar un mensaje "Listo", después del cual se completará el proceso Nodo.

Limitación : al resolver este problema, no puede llamar a setTimeout .

Sugerencia : necesita un contador.

Solución

Así es como resolvería este problema:

 let counter = 0; const intervalId = setInterval(() => { console.log('Hello World'); counter += 1; if (counter === 5) { console.log('Done'); clearInterval(intervalId); } }, 1000); 

Establecí 0 como el valor inicial del counter , y luego llamé a setInterval , que toma su id.

Una función diferida mostrará un mensaje y cada vez aumentará el contador en uno. Dentro de la función diferida, tenemos una instrucción if, que verificará si ya han pasado 5 iteraciones. Después de 5 iteraciones, el programa muestra "Listo" y borra el valor del intervalo utilizando la constante del intervalId . Del intervalId capturado. El intervalo de retraso es de 1000 ms.

¿Quién llama exactamente a las funciones diferidas?

Al usar JavaScript, this dentro de una función regular, como esta, por ejemplo:

 function whoCalledMe() { console.log('Caller is', this); } 

el valor en this coincidirá con la persona que llama . Si define la función anterior dentro del Node REPL, entonces el objeto global llamará. Si define una función en la consola del navegador, el objeto de la window llamará.

Definamos una función como una propiedad de un objeto para hacerlo un poco más claro:

 const obj = { id: '42', whoCalledMe() { console.log('Caller is', this); } }; //     : obj.whoCallMe 

Ahora, cuando usaremos directamente el enlace cuando obj.whoCallMe con la función obj.whoCallMe , el objeto obj (identificado por su id ) actuará como la persona que llama:



Ahora la pregunta es: ¿quién será la persona que llama si pasa el enlace a obj.whoCallMe a setTimetout ?

 //       ?? setTimeout(obj.whoCalledMe, 0); 

¿Quién es la persona que llama en este caso?

La respuesta diferirá según dónde se ejecute la función del temporizador. En este caso, la dependencia de quién es la persona que llama es simplemente inaceptable. Perderá el control sobre la persona que llama, porque dependerá de la implementación del temporizador que en este caso llama a su función. Si prueba este código en un Node REPL, el objeto Timeout será el que llama:



Tenga en cuenta: esto es importante solo cuando el JavaScript this utiliza dentro de las funciones regulares. Al usar las funciones de flecha, la persona que llama no debería molestarte en absoluto.

Temporizadores Problema # 3

Escriba un script que muestre continuamente un mensaje de "Hola mundo" con diferentes demoras. Comience con un retraso de un segundo y luego aumente en un segundo en cada iteración. En la segunda iteración, el retraso será de 2 segundos. En el tercero, tres, y así sucesivamente.

Incluya un retraso en el mensaje que se muestra. Deberías obtener algo como esto:

Hello World. 1
Hello World. 2
Hello World. 3
...


Limitaciones : las variables solo se pueden definir usando const. Usar let o var no lo es.

Solución

Dado que la duración de la demora en esta tarea es variable, no puede usar setInterval aquí, pero puede configurar manualmente la ejecución de intervalos usando setTimeout dentro de una llamada recursiva. La primera función ejecutada con setTimeout creará el siguiente temporizador, y así sucesivamente.

Además, dado que no puede usar let / var , no podemos tener un contador para aumentar el retraso de cada llamada recursiva; en su lugar, puede usar los argumentos de una función recursiva para realizar un incremento durante una llamada recursiva.

Aquí se explica cómo resolver este problema:

 const greeting = delay => setTimeout(() => { console.log('Hello World. ' + delay); greeting(delay + 1); }, delay * 1000); greeting(1); 

Temporizadores Tarea # 4

Escriba una secuencia de comandos que muestre el mensaje "Hola mundo" con la misma estructura de retraso que en la tarea n.º 3, pero esta vez en grupos de 5 mensajes, y el grupo tendrá un intervalo de retraso principal. Para el primer grupo de 5 mensajes, seleccionamos el retraso inicial de 100 ms, para el siguiente - 200 ms, para el tercero - 300 ms y así sucesivamente.

Así es como debería funcionar este script:

  • A 100 ms, el script muestra "Hola Mundo" por primera vez, y lo hace 5 veces con un intervalo que aumenta en 100 ms. El primer mensaje aparecerá después de 100 ms, el segundo después de 200 ms, etc.
  • Después de los primeros 5 mensajes, el script debería aumentar el retraso principal en 200 ms. Por lo tanto, el sexto mensaje se mostrará después de 500 ms + 200 ms (700 ms), séptimo - 900 ms, octavo mensaje - después de 1100 ms, y así sucesivamente.
  • Después de 10 mensajes, el script debería aumentar el intervalo de retraso principal en 300 ms. El undécimo mensaje debe mostrarse después de 500 ms + 1000 ms + 300 ms (18000 ms). El mensaje 12 debe mostrarse después de 2100 ms, etc.

De acuerdo con este principio, el programa debería funcionar indefinidamente.

Incluya un retraso en el mensaje que se muestra. Deberías obtener algo como esto (sin comentarios):

Hello World. 100 // 100
Hello World. 100 // 200
Hello World. 100 // 300
Hello World. 100 // 400
Hello World. 100 // 500
Hello World. 200 // 700
Hello World. 200 // 900
Hello World. 200 // 1100
...


Limitaciones : puede usar solo llamadas para establecer setInterval (y no para establecer setTimeout ) y solo UNA if .

Solución

Como solo podemos trabajar con llamadas setInterval , aquí debemos usar la recursividad y también aumentar el retraso de la próxima llamada setInterval . Además, necesitamos la if para que esto suceda solo después de 5 llamadas a esta función recursiva.

Aquí hay una posible solución:

 let lastIntervalId, counter = 5; const greeting = delay => { if (counter === 5) { clearInterval(lastIntervalId); lastIntervalId = setInterval(() => { console.log('Hello World. ', delay); greeting(delay + 100); }, delay); counter = 0; } counter += 1; }; greeting(100); 

Gracias a todos los que lo leyeron.

Source: https://habr.com/ru/post/es426709/


All Articles