
Cómo comenzar rápidamente con VUE3.0: comenzar a aprender
Cuando se trata de Node.js, creo que la mayoría de los ingenieros de front-end pensarán en desarrollar servidores basados en él. Solo necesita dominar JavaScript para convertirse en una pila completa. ingeniero, pero de hecho, el significado de Node.js Y eso no es todo.
Para muchos lenguajes de alto nivel, los permisos de ejecución pueden llegar al sistema operativo, pero JavaScript que se ejecuta en el lado del navegador es una excepción. El entorno sandbox creado por el navegador encierra a los ingenieros de front-end en una torre de marfil en el mundo de la programación. Sin embargo, la aparición de Node.js ha compensado esta deficiencia y los ingenieros de front-end también pueden llegar al fondo del mundo de la informática.
Por lo tanto, la importancia de Nodejs para los ingenieros de front-end no es solo proporcionar capacidades de desarrollo de pila completa, sino más importante aún, abrir una puerta al mundo subyacente de las computadoras para los ingenieros de front-end. Este artículo abre esta puerta al analizar los principios de implementación de Node.js.
Hay más de una docena de dependencias en el directorio /deps del almacén de código fuente de Node.js, incluidos módulos escritos en lenguaje C (como libuv, V8) y módulos escritos en lenguaje JavaScript (como acorn , complementos de bellota).

Los más importantes son los módulos correspondientes a los directorios v8 y uv. V8 en sí no tiene la capacidad de ejecutarse de forma asincrónica, pero se implementa con la ayuda de otros subprocesos en el navegador. Es por eso que a menudo decimos que js es de un solo subproceso, porque su motor de análisis solo admite código de análisis sincrónico. Pero en Node.js, la implementación asincrónica se basa principalmente en libuv. Centrémonos en analizar el principio de implementación de libuv.
libuv es una biblioteca de E/S asíncrona escrita en C que admite múltiples plataformas. Resuelve principalmente el problema de las operaciones de E/S que provocan fácilmente bloqueos. Originalmente fue desarrollado específicamente para su uso con Node.js, pero desde entonces ha sido utilizado por otros módulos como Luvit, Julia y pyuv. La siguiente figura es el diagrama de estructura de libuv.

libuv tiene dos métodos de implementación asincrónica, que son las dos partes seleccionadas por el cuadro amarillo a la izquierda y derecha de la imagen de arriba.
La parte izquierda es el módulo de E/S de red, que tiene diferentes mecanismos de implementación en diferentes plataformas. Los sistemas Linux usan epoll para implementarlo, OSX y otros sistemas BSD usan KQueue, los sistemas SunOS usan puertos de eventos y los sistemas Windows usan IOCP. Dado que involucra la API subyacente del sistema operativo, es más complicado de entender, por lo que no lo presentaré aquí.
La parte derecha incluye el módulo de E/S de archivos, el módulo DNS y el código de usuario, que implementa operaciones asincrónicas a través del grupo de subprocesos. La E/S de archivos es diferente de la E/S de red. libuv no depende de la API subyacente del sistema, sino que realiza operaciones de E/S de archivos bloqueados en el grupo de subprocesos global.
La siguiente figura es el diagrama de flujo de trabajo del sondeo de eventos proporcionado por el sitio web oficial de libuv, analicémoslo junto con el código.

El código central del bucle de eventos libuv se implementa en la función uv_run (). Lo siguiente es parte del código central del sistema Unix. Aunque está escrito en lenguaje C, es un lenguaje de alto nivel como JavaScript, por lo que no es demasiado difícil de entender. La mayor diferencia pueden ser los asteriscos y las flechas. Podemos simplemente ignorar los asteriscos. Por ejemplo, el bucle uv_loop_t* en el parámetro de función puede entenderse como un bucle variable de tipo uv_loop_t. La flecha "→" puede entenderse como el punto ".", por ejemplo, loop→stop_flag puede entenderse como loop.stop_flag.
int uv_run(uv_loop_t* bucle, modo uv_run_mode) {
...
r = uv__loop_alive(bucle);
if (!r) uv__update_time(bucle);
mientras (r != 0 && bucle - >stop_flag == 0) {
uv__update_time(bucle);
uv__run_timers(bucle);
ran_pending = uv__run_pending(bucle);
uv__run_idle(bucle);
uv__run_prepare(bucle);...uv__io_poll(bucle, tiempo de espera);
uv__run_check(bucle);
uv__run_closing_handles(bucle);...
}....
} uv__loop_alive
Esta función se utiliza para determinar si el sondeo de eventos debe continuar. Si no hay ninguna tarea activa en el objeto del bucle, devolverá 0 y saldrá del bucle.
En lenguaje C, esta "tarea" tiene un nombre profesional, es decir, "manejar", que puede entenderse como una variable que apunta a la tarea. Los identificadores se pueden dividir en dos categorías: solicitud y identificador, que representan identificadores de ciclo de vida corto y identificadores de ciclo de vida largo, respectivamente. El código específico es el siguiente:
static int uv__loop_alive(const uv_loop_t * loop) {
return uv__has_active_handles(bucle) || uv__has_active_reqs(bucle) || bucle - >closing_handles != NULL;
} uv__update_time
Para reducir la cantidad de llamadas al sistema relacionadas con el tiempo, esta función se utiliza para almacenar en caché la hora actual del sistema. La precisión es muy alta y puede alcanzar el nivel de nanosegundos, pero la unidad aún es milisegundos.
El código fuente específico es el siguiente:
UV_UNUSED(static void uv__update_time(uv_loop_t * loop)) {
bucle - >tiempo = uv__hrtime(UV_CLOCK_FAST) / 1000000;
} uv__run_timers
ejecuta las funciones de devolución de llamada que alcanzan el umbral de tiempo en setTimeout() y setInterval(). Este proceso de ejecución se implementa mediante un recorrido del bucle for. Como puede ver en el código siguiente, la devolución de llamada del temporizador se almacena en los datos de una estructura de montón mínima y sale cuando el montón mínimo está vacío o no ha alcanzado el umbral de tiempo. .
Elimine el temporizador antes de ejecutar la función de devolución de llamada del temporizador. Si se establece la repetición, debe agregarse al montón mínimo nuevamente y luego se ejecuta la devolución de llamada del temporizador.
El código específico es el siguiente:
void uv__run_timers(uv_loop_t * loop) {
estructura montón_nodo * montón_nodo;
uv_timer_t * manejar;
para (;;) {
montón_nodo = montón_min(timer_heap(bucle));
si (heap_node == NULL) se rompe;
manejar = contenedor_of(heap_node, uv_timer_t, heap_node);
if (manejar - >tiempo de espera > bucle - >tiempo) descanso;
uv_timer_stop(manejar);
uv_timer_again(manejar);
manejar ->timer_cb(manejar);
}
} uv__run_pending
atraviesa todas las funciones de devolución de llamada de E/S almacenadas en pendiente_queue y devuelve 0 cuando pendiente_queue está vacía; de lo contrario, devuelve 1 después de ejecutar la función de devolución de llamada en pendiente_queue;
El código es el siguiente:
static int uv__run_pending(uv_loop_t * loop) {
COLA * q;
COLA pq;
uv__io_t * w;
if (QUEUE_EMPTY( & loop - >pending_queue)) devuelve 0;
QUEUE_MOVE( & bucle - >cola_pendiente, &pq);
mientras (!QUEUE_EMPTY(&pq)) {
q = QUEUE_HEAD(& pq);
QUEUE_REMOVE(q);
QUEUE_INIT(q);
w = QUEUE_DATA(q, uv__io_t, pendiente_cola);
w - >cb(bucle, w, POLLOUT);
}
devolver 1;
} Las tres funciones uvrun_idle / uvrun_prepare / uv__run_check
se definen a través de una función macro UV_LOOP_WATCHER_DEFINE. La función macro puede entenderse como una plantilla de código o una función utilizada para definir funciones. La función macro se llama tres veces y los valores del parámetro de nombre preparar, verificar e inactivo se pasan respectivamente. Al mismo tiempo, se definen tres funciones, uvrun_idle, uvrun_prepare y uv__run_check.
Por lo tanto, su lógica de ejecución es consistente. Todos recorren y extraen los objetos en el bucle de cola->name##_handles de acuerdo con el principio de primero en entrar, primero en salir, y luego ejecutan la función de devolución de llamada correspondiente.
#define UV_LOOP_WATCHER_DEFINE(nombre, tipo)
void uv__run_##nombre(uv_loop_t* bucle) {
uv_##nombre##_t* h;
cola cola;
COLA* q;
QUEUE_MOVE(&loop->nombre##_handles, &queue);
mientras (!QUEUE_EMPTY(&cola)) {
q = QUEUE_HEAD(&cola);
h = QUEUE_DATA(q, uv_##nombre##_t, cola);
QUEUE_REMOVE(q);
QUEUE_INSERT_TAIL(&loop->nombre##_handles, q);
h->nombre##_cb(h);
}
}
UV_LOOP_WATCHER_DEFINE(preparar, PREPARAR)
UV_LOOP_WATCHER_DEFINE(verificar, VERIFICAR)
UV_LOOP_WATCHER_DEFINE(idle, IDLE) uv__io_poll
uv__io_poll se utiliza principalmente para sondear operaciones de E/S. La implementación específica variará según el sistema operativo. Tomamos el sistema Linux como ejemplo para el análisis.
La función uv__io_poll tiene una gran cantidad de código fuente. El núcleo son dos piezas de código de bucle. Parte del código es el siguiente:
void uv__io_poll(uv_loop_t * loop, int timeout) {
while (!QUEUE_EMPTY( & bucle - >watcher_queue)) {
q = QUEUE_HEAD( & bucle - >watcher_queue);
QUEUE_REMOVE(q);
QUEUE_INIT(q);
w = QUEUE_DATA(q, uv__io_t, watcher_queue);
e.eventos = w ->peventos;
e.data.fd = w ->fd;
if (w - >eventos == 0) op = EPOLL_CTL_ADD;
de lo contrario op = EPOLL_CTL_MOD;
if (epoll_ctl(bucle - >backend_fd, op, w - >fd, &e)) {
si (errno! = EEXIST) abortar();
if (epoll_ctl(loop - >backend_fd, EPOLL_CTL_MOD, w - >fd, &e)) abort();
}
w - >eventos = w - >peventos;
}
para (;;) {
para (i = 0; i < nfds; i++) {
pe = eventos + i;
fd = pe ->datos.fd;
w = bucle - >observadores[fd];
pe - >eventos &= w - >peventos | POLLERR |
if (pe - >eventos == POLLERR || pe - >eventos == POLLHUP) pe - >eventos |= w - >pevents & (POLLIN | POLLOUT | UV__POLLRDHUP | UV__POLLPRI);
si (pe - >eventos != 0) {
if (w == &loop - >signal_io_watcher) have_signals = 1;
else w - >cb(bucle, w, pe - >eventos);
eventos++;
}
}
if (have_signals != 0) loop - >signal_io_watcher.cb(loop, &loop - >signal_io_watcher, POLLIN);
}....
} En el bucle while, recorra la cola de observadores watcher_queue, saque el evento y el descriptor de archivo y asígnelos al objeto de evento e, y luego llame a la función epoll_ctl para registrar o modificar el evento epoll.
En el bucle for, el descriptor de archivo que espera en epoll se extraerá y se asignará a nfds, y luego se atravesará nfds para ejecutar la función de devolución de llamada.
uv__run_closing_handles
atraviesa la cola que espera ser cerrada, cierra identificadores como stream, tcp, udp, etc., y luego llama a close_cb correspondiente al identificador. El código es el siguiente:
static void uv__run_closing_handles(uv_loop_t * loop) {
uv_handle_t * p;
uv_handle_t * q;
p = bucle ->cierre_handles;
bucle ->cierre_handles = NULL;
mientras (p) {
q = p ->siguiente_cierre;
uv__finish_close(p);
pag = q;
}
} Aunque Process.nextTick y Promise son API asincrónicas, no forman parte del sondeo de eventos. Tienen sus propias colas de tareas, que se ejecutan después de que se completa cada paso del sondeo de eventos. Entonces, cuando usamos estas dos API asincrónicas, debemos prestar atención. Si se realizan tareas largas o recursivas en la función de devolución de llamada entrante, el sondeo de eventos se bloqueará, "matando de hambre" las operaciones de E/S.
El siguiente código es un ejemplo en el que la función de devolución de llamada de fs.readFile no se puede ejecutar llamando recursivamente a prcoess.nextTick.
fs.readFile('config.json', (err, datos) = >{...
}) recorrido constante = () = >{
proceso.nextTick (atravesar)
} Para resolver este problema, puede usar setImmediate en su lugar, porque setImmediate ejecutará la cola de funciones de devolución de llamada en el bucle de eventos. La cola de tareas Process.nextTick tiene una prioridad más alta que la cola de tareas Promise. Por motivos específicos, consulte el siguiente código:
function ProcessTicksAndRejections() {.
dejar tac;
hacer {
mientras (tock = cola.shift()) {
const asyncId = tock[async_id_symbol];
emitBefore(asyncId, tock[trigger_async_id_symbol], tock);
intentar {
devolución de llamada constante = tock.callback;
si (tock.args === indefinido) {
llamar de vuelta();
} demás {
argumentos const = tock.args;
cambiar (args. longitud) {
caso 1:
devolución de llamada (argumentos [0]);
romper;
caso 2:
devolución de llamada (argumentos [0], argumentos [1]);
romper;
caso 3:
devolución de llamada (argumentos [0], argumentos [1], argumentos [2]);
romper;
caso 4:
devolución de llamada (argumentos [0], argumentos [1], argumentos [2], argumentos [3]);
romper;
por defecto:
devolución de llamada (... argumentos);
}
}
} finalmente {
si (destroyHooksExist()) emitDestroy(asyncId);
}
emitirDespués(asyncId);
}
ejecutarMicrotareas();
} while (! cola . isEmpty () || ProcessPromiseRejections());
setHasTickScheduled(falso);
setHasRejectionToWarn(falso);
} Como se puede ver en la función ProcessTicksAndRejections (), la función de devolución de llamada de la cola se elimina primero a través del bucle while, y la función de devolución de llamada en la cola se agrega a través de Process.nextTick. Cuando finaliza el ciclo while, se llama a la función runMicrotasks() para ejecutar la función de devolución de llamada de Promise.
la estructura central de Node.js que se basa en libuv se puede dividir en dos partes. Una parte es la E/S de red y la implementación subyacente dependerá de diferentes API del sistema según los diferentes sistemas operativos. /O, DNS y código de usuario. Esta parte utiliza el grupo de subprocesos para el procesamiento.
El mecanismo central de libuv para manejar operaciones asincrónicas es el sondeo de eventos. El sondeo de eventos se divide en varios pasos. La operación general es recorrer y ejecutar la función de devolución de llamada en la cola.
Finalmente, se menciona que el proceso API asincrónico.nextTick y Promise no pertenecen al sondeo de eventos. El uso inadecuado hará que el sondeo de eventos se bloquee. Una solución es usar setImmediate en su lugar.