Ivan Tulup: asíncrono en JS bajo el capó

¿Conoces a Ivan Tulup? Lo más probable es que sí, todavía no sabes qué tipo de persona es esta, y debes cuidar mucho el estado de su sistema cardiovascular.

Acerca de esto y cómo funciona el asincronismo en JS bajo el capó, cómo funciona Event Loop en los navegadores y Node.js, ¿hay alguna diferencia y tal vez Mikhail Bashurov ( SaitoNakamura ) dijo cosas similares en su informe sobre RIT? ++. Nos complace compartir con ustedes la transcripción de esta presentación informativa.



Sobre el orador: Mikhail Bashurov es un desarrollador web fullstack en JS y .NET de Luxoft. Le encanta la hermosa interfaz de usuario, las pruebas ecológicas, la transpilación, la compilación, la técnica de compilación que permite y mejorar la experiencia de desarrollo.

Nota del editor: el informe de Mikhail estuvo acompañado no solo de diapositivas, sino también de un proyecto de demostración en el que puede hacer clic en los botones y observar de forma independiente la ejecución de la combinación aleatoria. La mejor opción sería abrir la presentación en una pestaña adyacente y consultarla periódicamente, pero el texto también proporcionará enlaces a páginas específicas. Y ahora pasamos la palabra al orador, disfruta leyendo.


Abuelo Ivan Tulup


Tenía una candidatura para Ivan Tulup.



Pero decidí seguir un camino más conformista, ¡así que conoce al abuelo Ivan Tulup!



De hecho, solo se necesitan saber dos cosas sobre él:

  1. Le gusta jugar a las cartas.
  2. Él, como todas las personas, tiene un corazón y late.

Hechos de ataque al corazón


Es posible que haya escuchado que los casos de enfermedades cardíacas y mortalidad por ellos se han vuelto más frecuentes recientemente. Probablemente la enfermedad cardíaca más común es un ataque cardíaco, es decir, un ataque cardíaco.

¿Qué tiene de interesante el ataque al corazón?

  • Muy a menudo, ocurre el lunes por la mañana.
  • En personas solteras, el riesgo de un ataque cardíaco es dos veces mayor. Aquí, quizás, el punto está únicamente en la correlación, y no en una relación causal. Desafortunadamente (o afortunadamente), sin embargo, esto es así.
  • Diez conductores murieron de un ataque al corazón durante la conducción (¡trabajo aparentemente muy nervioso!).
  • Un ataque cardíaco es una necrosis del músculo cardíaco causada por la falta de flujo sanguíneo.

Tenemos una arteria coronaria que lleva sangre al músculo (miocardio). Si la sangre comienza a fluir mal hacia ella, el músculo muere gradualmente. Naturalmente, esto tiene un efecto extremadamente negativo en el corazón y su trabajo.

El abuelo Ivan Tulup también tiene un corazón y late. Pero nuestro corazón bombea sangre, y el corazón de Ivan Tulup bombea nuestro código y nuestras penas.

Tasky: un gran círculo de circulación sanguínea


¿Qué son las tareas? ¿Qué puede ser generalmente vago en un navegador? ¿Por qué se necesitan en absoluto?

Por ejemplo, ejecutamos código desde un script. Este es un latido del corazón, y ahora tenemos flujo sanguíneo. Hicimos clic en el botón y nos suscribimos al evento, el controlador de eventos para este evento escupió, la devolución de llamada que enviamos. Establecieron Timeout, Callback funcionó, otra tarea. Y así, en partes, un latido es una tarea.



Hay muchas fuentes diferentes de repollo, de acuerdo con la especificación hay muchas. Nuestro corazón continúa latiendo, y mientras late, todo está bien con nosotros.

Event Loop en el navegador: versión simplificada


Esto se puede representar en un diagrama muy simple.



  • Hay una tarea, la hemos completado.
  • Luego ejecutamos el renderizado del navegador.

Pero, de hecho, esto no es necesario, porque en algunos casos el navegador puede no procesar entre dos tareas.

Esto puede suceder, por ejemplo, si el navegador puede decidir agrupar múltiples tiempos de espera o múltiples eventos de desplazamiento. O en algún momento, algo sale mal, y el navegador decide en lugar de 60 fps (velocidad de fotogramas normal para que todo salga bien y sin problemas) para mostrar 30 fps. Por lo tanto, tendrá mucho más tiempo para ejecutar su código y otros trabajos útiles, podrá realizar varias descargas.

Por lo tanto, el renderizado no se realiza realmente después de cada tarea.

Tasky: clasificación


Hay dos tipos de operaciones potenciales:

  1. I / O enlazado;
  2. CPU atado.

El límite de la CPU es nuestro trabajo útil que hacemos (creer, mostrar, etc.)

Los límites de E / S son los puntos en los que podemos compartir nuestras tareas. Podría ser:

  • Tiempo de espera
Hicimos setTimeout 5000 ms, y solo esperamos estos 5000 ms, pero podemos hacer otro trabajo útil. Solo cuando pasa este tiempo, recibimos Callback y trabajamos un poco.

  • xhr / fetch.
Nos fuimos en línea. Mientras esperamos una respuesta de la red, solo esperamos, pero también podemos hacer algo útil.

  • Red (OBD).
O, por ejemplo, vamos a Network BD. También estamos hablando de Node.js, incluido, y si queremos ir a alguna parte de la red desde Node.js, esta es la misma tarea potencial de E / S (entrada / salida).

  • Archivo.
Lea el archivo: potencialmente no es una tarea vinculada a la CPU. En Node.js, se ejecuta en el grupo de subprocesos debido a una API de Linux ligeramente torcida, para ser honesto.

Entonces CPUbound es:

  • Por ejemplo, cuando hacemos un ciclo for of / for (;;) o pasamos a través de la matriz de alguna manera usando métodos adicionales: filtro, mapa, etc.
  • JSON.parse o JSON.stringify, es decir, serialización / deserialización de mensajes. Todo esto se hace en la CPU, no podemos esperar a que todo se ejecute mágicamente en alguna parte.
  • Contando hashes, es decir, por ejemplo, cripto minería.

Por supuesto, crypto también se puede extraer en la GPU, pero creo que, GPU, CPU, entiendes esta analogía.

Tasky: arritmia y trombo


Como resultado, resulta que nuestro corazón late: realiza una tarea, la segunda, la tercera, hasta que hacemos algo mal. Por ejemplo, revisamos una matriz de 1 millón de elementos y contamos la suma. Parece que esto no es tan difícil, pero puede llevar un tiempo tangible. Si constantemente tomamos un tiempo tangible sin liberar la tarea, nuestro render no se puede realizar. Se cernía sobre este anhelo, y todo comienza la arritmia.

Creo que todos entienden que la arritmia es una enfermedad cardíaca bastante desagradable. Pero aún puedes vivir con él. ¿Qué sucede si coloca una tarea que simplemente cuelga todo el bucle de eventos en un bucle sin fin? De alguna manera, colocas un coágulo de sangre en la coronaria o en otra arteria, y todo se volverá completamente triste. Lamentablemente, nuestro abuelo Ivan Tulup morirá.

Entonces el abuelo Ivan murió ...




Para nosotros, esto significa que toda la pestaña se congela por completo: no puede hacer clic en nada, y luego Chrome dice: "Aw, Snap!"

Esto es incluso mucho peor que los errores del sitio web cuando algo salió mal. Pero si todo se bloqueó, e incluso, probablemente, la CPU se cargó y el usuario generalmente se colgó, lo más probable es que nunca vuelva a visitar su sitio.

Por lo tanto, la idea es la siguiente: tenemos una tarea y no necesitamos colgar en esta tarea durante mucho tiempo. Necesitamos liberarlo rápidamente, para que el navegador, en todo caso, pueda renderizar (si lo desea). Si no quieres, ¡genial, baila!

Demo de Philip Roberts: Lupa de Philip Roberts


Considere un ejemplo :

$.on('button', 'click', function onClick(){ console.log('click'); }); setTimeout(function timeout() { console log("timeout"); }. 5000); console.log(“Hello world"); 

La esencia es esta: tenemos un botón, nos suscribimos a él (addEventListener), Timeout se llama durante 5 segundos e inmediatamente en la consola.log escribimos "Hello, world!", En setTimeout escribimos Timeout, en onClick escribimos Click.

¿Qué sucederá si lo ejecutamos y muchas veces hacemos clic en el botón? ¿Cuándo se ejecutará el tiempo de espera? Veamos la demo:


El código comienza a ejecutarse, se pone en la pila, se agota el tiempo de espera. Mientras tanto, hicimos clic en el botón. Al final de la cola, se agregaron varios eventos. Mientras Click se está ejecutando, Timeout espera, aunque han pasado 5 segundos.

Aquí, onClick es rápido, pero si pones una tarea más larga, entonces todo se congelará, como ya se explicó. Este es un ejemplo muy simplificado. Aquí hay un giro, pero en los navegadores, de hecho, no todo es así.

¿En qué orden se ejecutan los eventos? ¿Qué dice la especificación HTML?

Ella dice lo siguiente: tenemos 2 conceptos:

  1. fuente de tarea;
  2. cola de tareas

El origen de la tarea es un tipo de tarea. Esta puede ser la interacción del usuario, es decir, onClick, onChange, algo con lo que el usuario interactúa; o temporizadores, es decir, setTimeout y setInterval, o PostMessages; o incluso tipos completamente salvajes como el origen de la tarea de serialización de blob de lienzo, también un tipo separado.

La especificación dice que para la misma tarea se garantizará que las tareas de origen se ejecuten en el orden en que se agregan. Para todo lo demás, nada está garantizado, porque puede haber un número ilimitado de colas de tareas. El navegador decide cuántos habrá. Con la ayuda de la cola de tareas y su creación, el navegador puede priorizar ciertas tareas.

Prioridades del navegador y colas de tareas




Imagina que tenemos 3 líneas:

  1. interacción del usuario;
  2. tiempos de espera
  3. publicar mensajes

El navegador comienza a recibir tareas de estas colas:

  • Primero, toma la interacción con el usuario Focus - esto es muy importante - un latido se ha ido.
  • Luego toma postMessages , bueno, postMessages es una prioridad bastante alta, ¡genial!
  • El siguiente, onChange, también es nuevamente de la interacción del usuario en prioridad.
  • A continuación se envía onClick . La cola de interacción del usuario ha finalizado, hemos mostrado al usuario todo lo que se necesita.
  • Luego tomamos setInterval , agregamos postMessages.
  • setTimeout solo ejecutará el más reciente . Estaba en algún lugar al final de la línea.

Este es nuevamente un ejemplo muy simplificado y, desafortunadamente, nadie puede garantizar cómo funcionará esto en los navegadores , porque ellos mismos deciden todo esto. Debe probar esto usted mismo si desea averiguar de qué se trata.

Por ejemplo, postMessages tiene prioridad sobre setTimeout. Es posible que haya oído hablar de setImmediate, que, por ejemplo, en los navegadores IE, era solo nativo. Pero hay archivos polifónicos que se basan principalmente no en setTimeout, sino en crear un canal postMessages y suscribirse a él. Esto funciona generalmente más rápido porque los navegadores lo priorizan.

Bueno, estas tareas se llevan a cabo. ¿En qué punto terminamos nuestra tarea y entendemos que podemos tomar la siguiente o que podemos rendir?

Pila


La pila es una estructura de datos simple que funciona según el principio de "último en entrar, primero en salir", es decir "Puse el último, obtienes el primero" . La contraparte más cercana, probablemente real, es una baraja de cartas. Por lo tanto, a nuestro abuelo Ivan Tulup le encanta jugar a las cartas.



En el ejemplo anterior, en el que hay algún código, se puede introducir el mismo ejemplo en la presentación . En algún lugar llamamos handleClick, ingrese console.log, llame a showPopup y window. confirmar Formemos una pila.

  • Entonces, primero tomamos handleClick y enviamos la llamada a esta función a la pila, ¡genial!
  • Luego vamos a su cuerpo y lo ejecutamos.
  • Ponemos console.log en la pila e inmediatamente lo ejecutamos, porque todo está ahí para ejecutarlo.
  • Luego ponemos showConfirm - esta es una llamada a función - genial.
  • Ponemos funciones en la pila, ponemos su cuerpo, es decir, window.confirm.

No tenemos nada más, lo estamos haciendo. Aparecerá una ventana emergente: "¿Estás seguro?", Haz clic en "Sí", y todo saldrá de la pila. Ahora hemos terminado el cuerpo showConfirm y el cuerpo handleClick. Nuestra pila se borra y podemos pasar a la siguiente tarea. Pregunta: Bien, ahora sé que necesitas dividirlo todo en pedazos pequeños. ¿Cómo puedo, por ejemplo, hacer esto en el caso más elemental?

Particionar una matriz en trozos y procesarlos de forma asincrónica


Veamos el ejemplo más "de frente". Te advierto de inmediato: no intentes repetir esto en casa, no se compilará.



Tenemos una matriz grande, grande, y queremos calcular algo basado en ella, por ejemplo, para analizar algunos datos binarios. Simplemente podemos dividirlo en trozos: procese esta pieza, esto y esto. Seleccionamos el tamaño del fragmento, por ejemplo, 10 mil elementos, consideramos cuántos fragmentos tendremos. Tenemos una función parseData que va a la CPU y realmente puede hacer algo pesado. Luego dividimos la matriz en trozos, setTimeout (() => parseData (slice), 0).

En este caso, el navegador nuevamente podrá priorizar la interacción del Usuario y renderizar en el medio. Es decir, al menos liberas tu Event Loop, y continúa funcionando. Tu corazón continúa latiendo, y eso es bueno.

Pero este es realmente un ejemplo muy "de frente". Hay muchas API en los navegadores para ayudarlo a hacer esto de una manera más especializada.

Además de setTimeout y setInterval, hay API que van más allá de los límites, como, por ejemplo, requestAnimationFrame y requestIdleCallback.

Probablemente muchos estén familiarizados con requestAnimationFrame , e incluso ya lo usan. Se ejecuta antes de renderizar. Su encanto es que, en primer lugar, intenta ejecutar cada 60 fps (o 30 fps), y en segundo lugar, todo esto se hace inmediatamente antes de crear el Modelo de objetos CSS, etc.



Por lo tanto, incluso si tiene varios requestAnimationFrame, en realidad agruparán todos los cambios y el marco saldrá completo. En el caso de setTimeout, ciertamente no puede obtener tal garantía. Un setTimeout cambiará una cosa, la otra otra, y en el medio de la representación puede resbalar: tendrá un tirón de la pantalla u otra cosa. RequestAnimationFrame es excelente para esto.

Además de esto, también hay requestIdleCallback. Quizás escuchaste que se usa en React v16.0 (Fiber). RequestIdleCallback funciona de tal manera que si el navegador comprende que tiene tiempo entre cuadros (60 fps) para hacer algo útil, y al mismo tiempo ya han hecho todo: hicieron la tarea, requestAnimationFrame lo hizo, parece genial, entonces puede producir cuantos pequeños, digamos, 50 ms cada uno, para que pueda hacer algo (modo inactivo).

No está en el diagrama de arriba, porque no está ubicado en ningún lugar en particular. El navegador puede decidir colocarlo antes del marco, después del marco, entre requestAnimationFrame y el render, después de la tarea, antes de la tarea. Nadie puede garantizar esto.

Se le garantiza que si tiene un trabajo que no está relacionado con el cambio de DOM (porque entonces requestAnimationFrame es animación, etc.), aunque no es una prioridad súper, pero tangible, requestIdleCallback es su salida.

Entonces, si tenemos una operación de CPU larga, entonces podemos tratar de romperla en pedazos.

  • Si se trata de un cambio de DOM, use requestAnimationFrame.
  • Si se trata de una tarea no prioritaria, de corta duración y no difícil que no sobrecargará la CPU, solicite la llamada inactiva.
  • Si tenemos una gran tarea poderosa que debe realizarse constantemente, entonces vamos más allá de Event Loop y usamos WebWorkers. No hay otra manera

Tareas en navegadores:

  1. Aplasta todo en pequeñas tareas.
  2. Hay muchos tipos de tareas.
  3. Estos tipos priorizan las tareas mediante colas de especificación.
  4. Los navegadores deciden mucho y la única forma de entender cómo funciona es simplemente verificar si se está ejecutando uno u otro código.
  5. ¡Pero la especificación no siempre se respeta!

El problema es que nuestro Ivan Tulup es un viejo abuelo, porque las implementaciones de Event Loop en los navegadores también son muy antiguas. Se crearon antes de que se escribiera la especificación, por lo que, lamentablemente, la especificación se respeta en la medida en que. Incluso si lee cómo debería ser la especificación, nadie garantiza que todos los navegadores la admitan. Así que asegúrese de verificar en los navegadores cómo funciona esto realmente.

El abuelo Ivan Tulup en los navegadores es una persona poco predecible, con algunas características interesantes, debe recordar esto.

Terminator Santa: Mascot Loop en Node.js


Node.js es más como alguien así.



Porque, por un lado, es el mismo abuelo con barba, pero al mismo tiempo todo se distribuye en fases y está claramente pintado donde se hace.

Fases del bucle de eventos en Node.js:

  • temporizadores
  • devolución de llamada pendiente;
  • inactivo, preparar;
  • encuesta
  • comprobar
  • Cerrar devoluciones de llamada.

Todo, excepto el último, no tiene muy claro lo que significa. Las fases tienen nombres tan extraños, porque debajo del capó, como ya sabemos, tenemos a Libuv para gobernar a todos:

  • Linux - epoll / POSIX AIO;
  • BSD - kqueue;
  • Windows - IOCP;
  • Solaris - puertos de eventos.

Miles de todos ellos!

Además, Libuv también proporciona el mismo bucle de eventos. No tiene los detalles de Node.js, pero hay fases, y Node.js solo los usa. Pero por alguna razón tomó los nombres de allí.

Veamos qué significa realmente cada fase.

La fase de temporizadores realiza:


  • Temporizadores listos para devolución de llamada;
  • setTimeout y setInterval;
  • Pero NO setImmediate es una fase diferente.

Llamadas pendientes de fase


Antes de esto, la fase de documentación denominada devoluciones de llamada de E / S. Más recientemente, esta documentación se corrigió y dejó de contradecirse. Antes de esto, en un lugar se escribió que las devoluciones de llamada de E / S se ejecutan en esta fase, en otro, que en la fase de sondeo. Pero ahora todo está escrito allí inequívocamente y bien, así que lea la documentación: algo será mucho más comprensible.

En la fase de devolución de llamada pendiente, se ejecutan las devoluciones de llamada de algunas operaciones del sistema (error de TCP). Es decir, si en Unix hay un error en el socket TCP, en este caso no quiere tirarlo inmediatamente, sino en la devolución de llamada, que se ejecutará solo en esta fase. Eso es todo lo que necesitamos saber sobre ella. Prácticamente no nos interesa.

Fase inactiva, preparar


En esta fase, no podemos hacer nada, por lo que, en principio, lo olvidaremos.



Fase de encuesta


Esta es la fase más interesante en Node.js porque realiza el trabajo principal útil:

  • Realiza devoluciones de llamada de E / S (¡no está pendiente la fase de devolución de llamada!).
  • Esperando eventos de E / S;
  • Es genial hacer setImmediate;
  • Sin temporizadores;

Mirando hacia el futuro, setImmediate se ejecutará en la siguiente fase de verificación, es decir, garantizada antes de los temporizadores.

Y también la fase de sondeo controla el flujo del bucle de eventos. Por ejemplo, si no tenemos temporizadores, no hay setImmediate, es decir, nadie hizo el temporizador, setImmediate no llamó, simplemente bloqueamos en esta fase y esperamos el evento desde E / S, si algo nos llega, si hay alguna devolución de llamada si nos registramos para algo

¿Cómo se implementa un modelo sin bloqueo? Por ejemplo, en el mismo Epoll, podemos suscribirnos a un evento: abrir un socket y esperar a que se escriba algo en él. Además, el segundo argumento es el tiempo de espera, es decir Esperaremos a Epoll, pero si el tiempo de espera finaliza y el evento de E / S no llega, saldrá del tiempo de espera. Si un evento nos llega desde la red (alguien escribe en el socket), entonces vendrá.

Por lo tanto, la fase de sondeo elimina el montón (el montón es una estructura de datos que permite una entrega y entrega bien ordenadas) de la devolución de llamada más temprana, toma su tiempo de espera, escribe en este tiempo de espera y libera todo. Por lo tanto, incluso si nadie nos escribe en el socket, el tiempo de espera funcionará, volverá a la fase de sondeo y el trabajo continuará.

Es importante tener en cuenta que en la fase de encuesta hay un límite en la cantidad de devoluciones de llamada a la vez.

Es triste que en las fases restantes no lo sea. Si agrega 10 mil millones de tiempo de espera, agrega 10 mil millones de tiempo de espera. Por lo tanto, la siguiente fase es la fase de verificación.

Fase de verificación


Aquí es donde se ejecuta setImmediate. La fase es hermosa en ese setImmediate, llamado en la fase de sondeo, se garantiza que se ejecute antes que el temporizador. Porque el temporizador solo estará en el siguiente tic al principio y antes de la fase de sondeo. Por lo tanto, no podemos temer la competencia con otros temporizadores y usar esta fase para aquellas cosas que no queremos por alguna razón ejecutar en una devolución de llamada.

Devoluciones de llamada de cierre de fase


Esta fase no ejecuta todas nuestras devoluciones de llamada de cierre de socket y otros tipos:

 socket.on('close', …). 

Ella los ejecuta solo si este evento voló inesperadamente, por ejemplo, alguien en el otro extremo envió: "¡Todo, cierra el zócalo, ve desde aquí, Vasya!" Entonces esta fase funcionará, porque el evento es inesperado. Pero esto no nos afecta particularmente.

Procesamiento asincrónico incorrecto de fragmentos en Node.js


Qué sucederá si ponemos el mismo patrón que tomamos en los navegadores con setTimeout en Node.js, es decir, dividimos la matriz en fragmentos, para cada fragmento hacemos setTimeout - 0.

 const bigArray = [1..1_000_000] const chunks = getChunks(bigArray) const parseData = (slice) => // parse binary data for (chunk of chunks) { setTimeout(() => parseData(slice), 0) } 

¿Crees que hay algún problema con esto?

Ya me adelanté un poco cuando dije que si agrega 10 mil tiempos de espera (¡o 10 mil millones!), Habrá 10 mil temporizadores en la cola, y él los obtendrá y ejecutará; no hay protección contra esto: obtener, ejecutar, obtener ... para cumplir y así sucesivamente hasta el infinito.

Solo la fase de sondeo, si constantemente recibimos un evento de E / S, todo el tiempo alguien escribe algo en el socket para que podamos ejecutar al menos temporizadores y setImmediate, tiene protección de límite y depende del sistema. Es decir, diferirá en los diferentes sistemas operativos.

Desafortunadamente, otras fases, incluidos los temporizadores y setImmediate, no tienen dicha protección. Por lo tanto, si hace lo mismo que en el ejemplo, todo se congelará y no llegará a la fase de sondeo durante mucho tiempo.

¿Pero crees que algo cambiará si reemplazamos setTimeout (() => parseData (slice), 0) con setImmediate (() => parseData (slice))? - Naturalmente, no, tampoco hay protección en la fase de verificación allí.

Para resolver este problema, puede llamar al procesamiento recursivo .

 const parseData = (slice) => // parse binary data const recursiveAsyncParseData = (i) => { parseData(getChunk(i)) setImmediate(() => recursiveAsyncParseData(i + 1)) } recursiveAsyncParseData(0) 

La conclusión es que tomamos la función parseData y escribimos su llamada recursiva, pero no solo a nosotros mismos, sino a través de setImmediate. Cuando llamas a esto en la fase setImmediate, pasa al siguiente tic y no al actual. Por lo tanto, esto lanzará el bucle de eventos, irá más lejos en un círculo. Es decir, tenemos recursiveAsyncParseData, donde pasamos un cierto índice, obtenemos el fragmento por este índice, lo analizamos y luego colocamos setImmediate de cola con el siguiente índice. Llegará a nuestro próximo tic y podremos procesar todo esto recursivamente.

Es cierto que el problema es que esto todavía es algún tipo de tarea vinculada a la CPU. Quizás todavía pese de alguna manera y tome tiempo en Event Loop. Lo más probable es que desee que su Node.js esté limitado a E / S.
Por lo tanto, es mejor usar algunas otras cosas, por ejemplo, el grupo fork / thread del proceso.

Ahora sabemos sobre Node.js que:

  • todo se distribuye en fases, bueno, lo sabemos claramente;
  • hay protección contra una fase de sondeo demasiado larga, pero no el resto;
  • los patrones de procesamiento recursivo se pueden aplicar para no bloquear Event Loop;
  • Pero es mejor usar fork de proceso, grupo de subprocesos, proceso hijo

También debe tener cuidado con el grupo de subprocesos, porque Node.js inicia las cosas allí, en particular, la resolución de DNS, porque para Linux, por alguna razón, la función de resolución de DNS no es asíncrona. Por lo tanto, debe ejecutarse en ThreadPool. En Windows, afortunadamente, no es así. Pero allí puedes leer archivos de forma asincrónica. En Linux, desafortunadamente, es imposible.

En mi opinión, el límite estándar es de 4 procesos en ThreadPool. Por lo tanto, si hace algo activamente allí, competirá con todos los demás, con fs y otros. Puede considerar aumentar ThreadPool, pero también con mucho cuidado. Entonces lea algo sobre este tema.

Microtask: circulación pulmonar


Tenemos tareas en Node.js y tareas en navegadores. Puede que ya hayas oído hablar de microtask. Veamos qué es y cómo funcionan, y comencemos con los navegadores.

Microtask en navegadores


Para comprender cómo funciona microtask, recurrimos al algoritmo de bucle de eventos de acuerdo con el estándar whatwg, es decir, vamos a la especificación y veamos cómo se ve todo.



Traduciendo al lenguaje humano, se parece a esto:

  • Toma la tarea gratis de nuestra línea
  • Lo llevamos a cabo
  • Llevamos a cabo el punto de control de microtask. De acuerdo, todavía no sabemos qué es, pero lo recordamos.
  • Actualizamos el renderizado (si es necesario) y volvemos al punto de partida.



Se llevan a cabo en el lugar indicado en el diagrama, y ​​en varios lugares más, de los cuales pronto aprenderemos. Es decir, la tarea ha terminado, se ejecutan microtask.

Fuentes de microtucks


  • Promesa, luego.

Importante: no se promete a sí mismo, es decir, Promise.then. La devolución de llamada que se colocó en ese momento es una microtask. Si llamó a 10, entonces, tiene 10 microcoches, 10 mil, entonces, 10 mil microcoches.

  • Observador de mutaciones.
  • Object.observe , que está en desuso y nadie lo necesita.

¿Cuántos usan el observador de mutaciones?

Creo que pocos usan el observador de mutaciones. Lo más probable es que Promise.then se use más, por eso lo consideraremos en el ejemplo.

Características del punto de control de microtask:

  • Hacemos todo , esto significa que llevamos a cabo todas las microtasks que tenemos en la cola hasta el final. No soltamos nada, solo tomamos y hacemos todo lo que sea, deberían ser micro, ¿verdad?
  • Todavía puede generar nuevas microtask en el proceso, y se ejecutarán en el mismo punto de control de microtask.
  • Lo que también es importante: se ejecutan no solo después de la ejecución de la tarea, sino también después de borrar la pila.

Este es un punto interesante. Resulta que es posible generar nuevas microtasks y todos las cumpliremos hasta el final. ¿A qué nos puede llevar esto?


Tenemos dos corazones Animé el primer corazón con animación JS, y el segundo con animación CSS. Hay otra gran característica llamada starveMicrotasks. Llamamos Promise.resolve, y luego ponemos la misma función en ese momento.
Vea en la presentación qué sucede si llama a esta función.

Sí, el corazón de JS se detendrá, porque agregamos una microtask, y luego agregamos una microtask en ella, y luego agregamos una microtask en ella ... Y así infinitamente.

Es decir, la llamada recursiva de microtucks colgará todo. ¡Pero parece que tengo todo asíncrono! Debería dejarlo ir, llamé a setTimeout allí. No! Desafortunadamente, debe tener cuidado con microtask, por lo que si usa una llamada recursiva de alguna manera, tenga cuidado: puede bloquear todo.

Además, como recordamos, microtask se ejecuta al final de la limpieza de la pila. Recordamos lo que es una pila. Resulta que tan pronto como salimos de nuestro código, se ejecutó la devolución de llamada setTimeout, eso es todo, las microtasks fueron allí. Esto puede conducir a efectos secundarios interesantes.

Considera un ejemplo .



Hay un botón y un contenedor gris en el que se encuentra. Nos suscribimos al clic del botón y del contenedor. , , , .

2 :

  1. Promise.resolve;
  2. .then, console.log('RO')

«FUS», – «DAH!» ( ).

, ? , , «FUS RO DAH!» Genial , .



, , . – . , - ?



! .



, .

, , , . , .

  • — buttonHandleClick, .
  • Promise.resolve. . , console.log('RO') . .
  • console.log('FUS').
  • buttonHandleClick . .
  • , (divHandleClick) , «DAH!».
  • HandleClick .

, . ? :

  • button.click(). .
  • button HandleClick.
  • Promise.resolve then. , Promise.resolve .
  • console.log «FUS».
  • buttonHandleClick , .

(click) , , . divHandleClick , , console.log('DAH!') . , .

, , button.click .
. , , . , , .

: () ( ). - , , stopPropagation. , , , , - , .

, - ( junior-) — «», promise, , then , - . , , : , , . . , - .

( 4) , . , , , , - . .

, :

  • Event Loop. Esto es desagradable.
  • , .

, . — , , .

Node.js


Node.js Promise.then process.nextTick. , — . , , , , .

process.nextTick


, process.nextTick, setImmediate? Node.js ?

. createServer, EventEmitter, , listen ( ), .

 const createServer = () => { const evEmitter = new EventEmitter() return { listen: port => { evEmitter.emit('listening', port) return evEmitter } } } const server = createServer().listen(8080) server.on('listening', () => console.log('listening')) 

, , 8080, listening console.log - .

, , - .

createServer, . listen, , . .

, , . ? process.nextTick: evEmitter.emit('listening', port) process.nextTick(() => evEmitter.emit('listening', port)).

, process.nextTick , . EventEmitter, . , , API, . process.nextTick, emit , userland . createServer, , listen, listening. — process.nextTick — ! , , .

process.nextTick . , .

, process.nextTick , Promise.then . process.nextTick , — , Event Loop, Node.js. , , .

process.nextTick , ghbvtybnm setImmediate , C++ .. process.nextTick .

Async/await


API — async/await, - . . , async/await Promise, Event Loop . , .



, !

Frontend Conf — 4 5 , . , :


Ven, será interesante!

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


All Articles