1. ¿Por qué está JavaScript único en el subproceso?
Una característica importante del lenguaje JavaScript es el enhebrado único, lo que significa que solo puede hacer una cosa al mismo tiempo. Entonces, ¿por qué JavaScript no puede tener múltiples hilos? Esto mejorará la eficiencia.
El enhebrado único de JavaScript está relacionado con su propósito. Como lenguaje de guiones de navegador, el objetivo principal de JavaScript es interactuar con los usuarios y operar DOM. Esto determina que solo puede ser un solo enhebrado, de lo contrario causará problemas de sincronización muy complejos. Por ejemplo, suponga que JavaScript tiene dos hilos al mismo tiempo, un hilo agrega contenido en un determinado nodo DOM, y el otro hilo elimina este nodo, ¿qué hilo debería tomar el navegador en este momento?
Por lo tanto, para evitar la complejidad, JavaScript es un solo hilo de su nacimiento, que se ha convertido en la característica central de este idioma y no cambiará en el futuro.
Para utilizar la potencia informática de las CPU de múltiples núcleos, HTML5 propuso el estándar del trabajador web, lo que permite que los scripts de JavaScript creen múltiples hilos, pero los hilos infantiles están completamente controlados por el hilo principal y no pueden operar el DOM. Por lo tanto, este nuevo estándar no cambia la naturaleza del enhebrado único de JavaScript.
2. Cola de tareas
El enhebrado único significa que todas las tareas deben colocarse en cola, y la tarea anterior se ejecutará antes de que se ejecute la próxima tarea. Si la tarea anterior lleva mucho tiempo, la siguiente tarea tiene que esperar.
Si la cola se debe a la gran cantidad de computación y la CPU está demasiado ocupada, estaría bien, pero muchas veces la CPU está inactiva porque el dispositivo IO (dispositivo de entrada y salida) es muy lento (como la operación AJAX lee datos de la red), y debe esperar a que salga el resultado antes de ejecutarlo.
El diseñador del lenguaje JavaScript se dio cuenta de que en este momento, la CPU podría ignorar por completo el dispositivo IO, suspender las tareas de espera y ejecutar las siguientes tareas primero. Espere hasta que el dispositivo IO devuelva el resultado, luego gire y continúe con la tarea suspendida.
Por lo tanto, JavaScript tiene dos métodos de ejecución: uno es que la CPU se ejecuta en secuencia, la tarea anterior finaliza y luego se ejecuta la siguiente tarea, que se llama ejecución sincrónica; La otra es que la CPU omite las tareas con un largo tiempo de espera y procesa primero las tareas posteriores, que se llama ejecución asíncrona. Los programadores eligen independientemente qué tipo de método de ejecución adoptar.
Específicamente, el mecanismo operativo de la ejecución asincrónica es el siguiente. (Lo mismo es cierto para la ejecución sincrónica, ya que puede considerarse como ejecución asincrónica sin tareas asíncronas).
(1) Todas las tareas se ejecutan en el hilo principal para formar una pila de contexto de ejecución.
(2) Además del hilo principal, también hay una "cola de tareas". El sistema coloca las tareas asincrónicas en la "cola de tareas" y luego continúa ejecutando tareas posteriores.
(3) Una vez que se ejecuten todas las tareas en la "pila de ejecución", el sistema leerá la "cola de tareas". Si en este momento, la tarea asíncrona ha terminado el estado de espera, ingresará a la pila de ejecución desde la "cola de tareas" y el reanudar la ejecución.
(4) El hilo principal sigue repitiendo el tercer paso anterior.
La siguiente figura es un diagrama esquemático del hilo principal y la cola de tareas.
Mientras el hilo principal esté vacío, leerá la "cola de tareas". Este es el mecanismo de ejecución de JavaScript. Este proceso se repetirá continuamente.
3. Eventos y funciones de devolución de llamada
La "cola de tareas" es esencialmente una cola de eventos (también entendida como una cola de mensajes). Cuando un dispositivo IO completa una tarea, agrega un evento a la "cola de tareas", lo que indica que las tareas asíncronas relevantes pueden ingresar la "pila de ejecución". El hilo principal lee la "cola de tareas", lo que significa leer qué eventos hay dentro.
Los eventos en la "cola de tareas" incluyen eventos además de eventos de dispositivos IO, pero también eventos generados por usuarios (como clics del mouse, desplazamiento de la página, etc.). Mientras se especifique la función de devolución de llamada, estos eventos ingresarán la "cola de tareas" cuando ocurran y esperen a que se lea el hilo principal.
La llamada "devolución de llamada" es el código que será colgado por el hilo principal. Las tareas asíncronas deben especificar una función de devolución de llamada. Cuando la tarea asíncrona regresa de la "cola de tareas" a la pila de ejecución, se ejecutará la función de devolución de llamada.
"Task Queue" es una estructura de datos en primer lugar, con eventos que se clasifican primero y se prefieren regresar al hilo principal. El proceso de lectura del hilo principal es básicamente automático. Mientras se borre la pila de ejecución, el primer evento en la "cola de tareas" volverá automáticamente al hilo principal. Sin embargo, debido a la función de "temporizador" mencionada más adelante, el hilo principal debe verificar el tiempo de ejecución, y algunos eventos deben volver al hilo principal en el momento especificado.
4. Bucle de eventos
El hilo principal lee eventos de la "cola de tareas". Este proceso está en bucle continuamente, por lo que todo el mecanismo de ejecución también se llama bucle de eventos.
Para comprender mejor el bucle de eventos, consulte la imagen a continuación (citado del discurso de Philip Roberts "Ayuda, estoy atrapado en un bucle de evento").
En la figura anterior, cuando se ejecuta el hilo principal, genera montón y pila. El código en la pila llama a varias API externas, que agregan varios eventos (haz clic, cargan, realizados) a la "cola de tareas". Mientras se ejecute el código en la pila, el hilo principal leerá la "cola de tareas" y ejecutará las funciones de devolución de llamada correspondientes a esos eventos a su vez.
Ejecute el código en la pila, siempre ejecutado antes de leer la "cola de tareas". Consulte el siguiente ejemplo.
La copia del código es la siguiente:
var req = new xmlhttprequest ();
req.open ('get', url);
req.onload = function () {};
req.Onerror = function () {};
req.send ();
El método Req.Send en el código anterior es una operación AJAX para enviar datos al servidor. Es una tarea asincrónica, lo que significa que el sistema leerá la "cola de tareas" solo después de que se ejecute todo el código en el script actual. Entonces, es equivalente al siguiente método de escritura.
La copia del código es la siguiente:
var req = new xmlhttprequest ();
req.open ('get', url);
req.send ();
req.onload = function () {};
req.Onerror = function () {};
Es decir, las partes de la función de devolución de llamada especificada (Onload y OnError) no son importantes antes o después del método Send (), porque son parte de la pila de ejecución, y el sistema siempre las ejecutará antes de leer la "cola de tareas".
5. Temporizador
Además de colocar tareas asíncronas, la "cola de tareas" también tiene otra función, que es colocar eventos cronometrados, es decir, especificar cuánto tiempo se ejecutará cierto código después. Esto se llama función de "temporizador", que es el código ejecutado regularmente.
La función del temporizador se completa principalmente por dos funciones: setTimeOut () y setInterval (). Sus mecanismos internos de carrera son exactamente los mismos. La diferencia es que el código especificado por el primero se ejecuta a la vez, mientras que el segundo se ejecuta repetidamente. Lo siguiente discute principalmente SetTimeOut ().
setTimeOut () acepta dos parámetros, el primero es la función de devolución de llamada, y la segunda es el número de milisegundos para posponer la ejecución.
La copia del código es la siguiente:
console.log (1);
setTimeOut (function () {console.log (2);}, 1000);
console.log (3);
El resultado de la ejecución del código anterior es 1, 3, 2, porque SetTimeout () retrasa la segunda línea hasta después de 1000 milisegundos.
Si el segundo parámetro de setTimeOut () se establece en 0, significa que la función de devolución de llamada especificada (0 milisegundos) se ejecuta inmediatamente después de que se haya ejecutado el código actual (se borra la pila de ejecución).
La copia del código es la siguiente:
setTimeOut (function () {console.log (1);}, 0);
console.log (2);
Los resultados de ejecución del código anterior son siempre 2 y 1, porque el sistema ejecutará la función de devolución de llamada en la "cola de tareas" solo después de ejecutar la segunda línea.
El estándar HTML5 especifica que el valor mínimo (intervalo más corto) del segundo parámetro de setTimeOut () no debe ser inferior a 4 milisegundos. Si es más bajo que este valor, aumentará automáticamente. Antes de esto, los navegadores más antiguos establecen el intervalo mínimo a 10 milisegundos.
Además, para esos cambios DOM (especialmente las piezas que involucran la re-renderización de la página), generalmente no se ejecutan de inmediato, sino cada 16 milisegundos. En este momento, el efecto de usar requestAnimationFrame () es mejor que setTimeOut ().
Cabe señalar que SetTimeout () solo inserta el evento en la "cola de tareas". Debe esperar hasta ejecutar el código actual (pila de ejecución) antes de que el hilo principal ejecute la función de devolución de llamada que especifica. Si el código actual lleva mucho tiempo, puede llevar mucho tiempo esperar, por lo que no hay garantía de que la función de devolución de llamada se ejecutará en el momento especificada por SetTimeOut ().
6. Node.js Event Loop
Node.js también es un bucle de eventos único, pero su mecanismo de ejecución es diferente al del entorno del navegador.
Consulte el diagrama a continuación (autor @BusyRich).
Según la figura anterior, el mecanismo de ejecución de Node.js es el siguiente.
(1) Scripts de JavaScript del motor V8.
(2) El código analizado llama a la API del nodo.
(3) La biblioteca libuv es responsable de la ejecución de la API del nodo. Asigna diferentes tareas a diferentes hilos, forma un bucle de eventos y devuelve los resultados de ejecución de la tarea al motor V8 de manera asincrónica.
(4) El motor V8 devuelve el resultado al usuario.
Además de los dos métodos SetTimeOut y SetInterval, Node.js también proporciona otros dos métodos relacionados con la "cola de tareas": Process.NextTick y SetimMediate. Pueden ayudarnos a profundizar nuestra comprensión de las "colas de tareas".
El método Process.NextTick puede activar la función de devolución de llamada al final de la "pila de ejecución" actual antes de que el hilo principal lea la "cola de tareas" la próxima vez. Es decir, las tareas que especifican siempre ocurren antes de todas las tareas asincrónicas. El método SetMediate desencadena la función de devolución de llamada en la cola de la "cola de tareas" actual, es decir, la tarea que especifica siempre se ejecuta la próxima vez que el hilo principal lea la "cola de tareas", que es muy similar a SetTimeout (FN, 0). Consulte el siguiente ejemplo (a través de StackOverflow).
La copia del código es la siguiente:
Process.NextTick (function a () {
console.log (1);
process.nextTick (function b () {console.log (2);});
});
setTimeout (function timeOut () {
console.log ('Tiempo de espera disparado');
}, 0)
// 1
// 2
// Tiempo de espera disparado
En el código anterior, dado que la función de devolución de llamada especificada por el proceso. Esto significa que si hay declaraciones de múltiples procesos.
Ahora, veamos setimmediate.
La copia del código es la siguiente:
setimmediate (función a () {
console.log (1);
setimmediate (function b () {console.log (2);});
});
setTimeout (function timeOut () {
console.log ('Tiempo de espera disparado');
}, 0)
// 1
// Tiempo de espera disparado
// 2
En el código anterior, hay dos setimmediates. El primero setMediate especifica que la función de devolución de llamada A se activa en la cola de la "cola de tareas" actual (el siguiente "bucle de eventos"); Luego, SetTimeOut también especifica que el tiempo de espera de la función de devolución de llamada se activa en la cola de la "cola de tareas" actual, por lo que en el resultado de la salida, el tiempo de espera disparado se clasifica detrás de 1. En cuanto a la clasificación 2 detrás del tiempo de espera, se debe a que otra característica importante de Setimmedia: un "bucle de eventos" solo puede activar una función de devolución de llamada especificada por Setimmediate.
Hemos obtenido una diferencia importante de esto: múltiples procesos. Las declaraciones de nextTick siempre se ejecutan a la vez, mientras que los múltiples setimediates requieren varias veces para ejecutarse. De hecho, esto es exactamente por qué Node.js versión 10.0 agrega el método SetImmediate. De lo contrario, la llamada recursiva a Process.NextTick, como lo siguiente, será interminable, ¡y el hilo principal no leerá la "cola de eventos" en absoluto!
La copia del código es la siguiente:
process.nextTick (function foo () {
Process.NextTick (foo);
});
De hecho, ahora, si escribe un proceso recursivo.
Además, dado que la función de devolución de llamada especificada por Process.NextTick se activa en este "bucle de eventos" y SetimEdiate especifica que se activa en el siguiente "bucle de eventos", es obvio que el primero siempre ocurre antes que el segundo y también es más eficiente en ejecución (porque no hay necesidad de verificar la "cola de tareas").