Como começar rapidamente com VUE3.0: Começando a aprender
Quando se trata de Node.js, acredito que a maioria dos engenheiros front-end pensará em desenvolver servidores baseados nele. Você só precisa dominar JavaScript para se tornar um full-stack. engenheiro, mas na verdade, o significado do Node.js E isso não é tudo.
Para muitas linguagens de alto nível, as permissões de execução podem chegar ao sistema operacional, mas o JavaScript executado no lado do navegador é uma exceção. O ambiente sandbox criado pelo navegador sela os engenheiros de front-end em uma torre de marfim no mundo da programação. No entanto, o surgimento do Node.js compensou essa deficiência, e os engenheiros front-end também podem chegar ao fundo do mundo da informática.
Portanto, a importância do Nodejs para engenheiros front-end não é apenas fornecer recursos de desenvolvimento full-stack, mas, mais importante, abrir uma porta para o mundo subjacente dos computadores para engenheiros front-end. Este artigo abre essa porta analisando os princípios de implementação do Node.js.
Existem mais de uma dúzia de dependências no diretório /deps do warehouse do código-fonte do Node.js, incluindo módulos escritos em linguagem C (como libuv, V8) e módulos escritos em linguagem JavaScript (como bolota , plug-ins acorn). Como mostrado abaixo.
O mais importante deles são os módulos correspondentes aos diretórios v8 e uv. O V8 em si não tem a capacidade de ser executado de forma assíncrona, mas é implementado com a ajuda de outros threads no navegador. É por isso que costumamos dizer que js é de thread único, porque seu mecanismo de análise suporta apenas código de análise síncrona. Mas no Node.js, a implementação assíncrona depende principalmente do libuv. Vamos nos concentrar na análise do princípio de implementação do libuv.
libuv é uma biblioteca de E/S assíncrona escrita em C que suporta múltiplas plataformas. Ela resolve principalmente o problema de operações de E/S que causam bloqueio facilmente. Ele foi originalmente desenvolvido especificamente para uso com Node.js, mas desde então tem sido usado por outros módulos, como Luvit, Julia e pyuv. A figura abaixo é o diagrama de estrutura do libuv.
libuv possui dois métodos de implementação assíncronos, que são as duas partes selecionadas pela caixa amarela à esquerda e à direita da imagem acima.
A parte esquerda é o módulo de E/S de rede, que possui diferentes mecanismos de implementação em diferentes plataformas. Os sistemas Linux usam epoll para implementá-lo, OSX e outros sistemas BSD usam KQueue, os sistemas SunOS usam portas de eventos e os sistemas Windows usam IOCP. Como envolve a API subjacente do sistema operacional, é mais complicado de entender, por isso não vou apresentá-lo aqui.
A parte direita inclui o módulo de E/S de arquivo, o módulo DNS e o código do usuário, que implementa operações assíncronas por meio do pool de threads. A E/S de arquivo é diferente da E/S de rede. A libuv não depende da API subjacente do sistema, mas executa operações de E/S de arquivo bloqueadas no conjunto de threads global.
A figura a seguir é o diagrama do fluxo de trabalho da pesquisa de eventos fornecido pelo site oficial da libuv. Vamos analisá-lo junto com o código.
O código principal do loop de eventos libuv é implementado na função uv_run(). O seguinte é parte do código principal do sistema Unix. Embora esteja escrito em linguagem C, é uma linguagem de alto nível como JavaScript, por isso não é muito difícil de entender. A maior diferença pode ser os asteriscos e setas. Podemos simplesmente ignorar os asteriscos. Por exemplo, o loop uv_loop_t* no parâmetro de função pode ser entendido como um loop variável do tipo uv_loop_t. A seta "→" pode ser entendida como o ponto final ".", por exemplo, loop→stop_flag pode ser entendido como loop.stop_flag.
int uv_run(uv_loop_t* loop, modo uv_run_mode) { ... r = uv__loop_alive(loop); if (!r) uv__update_time(loop); enquanto (r!= 0 && loop - >stop_flag == 0) { uv__update_time(loop); uv__run_timers(loop); ran_pending = uv__run_pending(loop); uv__run_idle(loop); uv__run_prepare(loop);...uv__io_poll(loop, tempo limite); uv__run_check(loop); uv__run_closing_handles(loop);... }... }
uv__loop_alive
Esta função é usada para determinar se a pesquisa de eventos deve continuar. Se não houver nenhuma tarefa ativa no objeto loop, ela retornará 0 e sairá do loop.
Na linguagem C, essa “tarefa” possui um nome profissional, ou seja, “handle”, que pode ser entendido como uma variável que aponta para a tarefa. Os identificadores podem ser divididos em duas categorias: solicitação e identificador, que representam identificadores de ciclo de vida curto e identificadores de ciclo de vida longo, respectivamente. O código específico é o seguinte:
static int uv__loop_alive(const uv_loop_t * loop) { retornar uv__has_active_handles(loop) || uv__has_active_reqs(loop) || loop - >closing_handles != NULL; }
uv__update_time
Para reduzir o número de chamadas do sistema relacionadas ao tempo, esta função é usada para armazenar em cache a hora atual do sistema. A precisão é muito alta e pode atingir o nível de nanossegundos, mas a unidade ainda é milissegundos.
O código-fonte específico é o seguinte:
UV_UNUSED(static void uv__update_time(uv_loop_t * loop)) { loop - >tempo = uv__hrtime(UV_CLOCK_FAST) / 1000000; }
uv__run_timers
executa as funções de retorno de chamada que atingem o limite de tempo em setTimeout() e setInterval(). Este processo de execução é implementado por meio da travessia do loop for. Como você pode ver no código abaixo, o retorno de chamada do temporizador é armazenado nos dados de uma estrutura de heap mínimo. .
Remova o cronômetro antes de executar a função de retorno de chamada do cronômetro. Se a repetição estiver definida, ela precisará ser adicionada ao heap mínimo novamente e, em seguida, o retorno de chamada do cronômetro será executado.
O código específico é o seguinte:
void uv__run_timers(uv_loop_t * loop) { estrutura heap_node * heap_node; uv_timer_t * identificador; para (;;) { heap_node = heap_min(timer_heap(loop)); if (heap_node == NULL) quebra; identificador = container_of(heap_node, uv_timer_t, heap_node); if (handle - >timeout > loop - >time) break; uv_timer_stop(alça); uv_timer_again(alça); manusear ->timer_cb(manusear); } }
uv__run_pending
percorre todas as funções de retorno de chamada de E/S armazenadas em pendente_queue e retorna 0 quando pendente_queue está vazio; caso contrário, retorna 1 após executar a função de retorno de chamada em pendente_queue.
O código é o seguinte:
static int uv__run_pending(uv_loop_t * loop) { FILA * q; FILA pq; uv__io_t * w; if (QUEUE_EMPTY( & loop - >pending_queue)) retornar 0; QUEUE_MOVE( & loop - >pending_queue, &pq); enquanto (!QUEUE_EMPTY( & pq)) { q = QUEUE_HEAD(&pq); QUEUE_REMOVE(q); QUEUE_INIT(q); w = QUEUE_DATA(q, uv__io_t, pendente_queue); w - >cb(loop, w, POLLOUT); } retornar 1; }As três funções
uvrun_idle / uvrun_prepare / uv__run_check
são todas definidas através de uma função macro UV_LOOP_WATCHER_DEFINE A função macro pode ser entendida como um modelo de código, ou uma função usada para definir funções. A função macro é chamada três vezes e os valores dos parâmetros de nome preparar, verificar e inativo são transmitidos respectivamente. Ao mesmo tempo, três funções, uvrun_idle, uvrun_prepare e uv__run_check, são definidas.
Portanto, sua lógica de execução é consistente. Todos eles percorrem e retiram os objetos na fila loop->name##_handles de acordo com o princípio primeiro a entrar, primeiro a sair e, em seguida, executam a função de retorno de chamada correspondente.
#define UV_LOOP_WATCHER_DEFINE(nome, tipo) void uv__run_##nome(uv_loop_t*loop) { uv_##nome##_t* h; Fila QUEUE; FILA* q; QUEUE_MOVE(&loop->nome##_handles, &queue); enquanto (!QUEUE_EMPTY(&queue)) { q = QUEUE_HEAD(&queue); h = QUEUE_DATA(q, uv_##nome##_t, fila); QUEUE_REMOVE(q); QUEUE_INSERT_TAIL(&loop->nome##_handles, q); h->nome##_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 é usado principalmente para pesquisar operações de E/S. A implementação específica irá variar dependendo do sistema operacional. Tomamos o sistema Linux como exemplo para análise.
A função uv__io_poll tem muito código-fonte. O núcleo é composto por duas partes do código de loop:
void uv__io_poll(uv_loop_t * loop, int timeout) {. while (!QUEUE_EMPTY( & loop - >watcher_queue)) { q = QUEUE_HEAD( & loop - >watcher_queue); QUEUE_REMOVE(q); QUEUE_INIT(q); w = QUEUE_DATA(q, uv__io_t, watcher_queue); e.events = w ->pevents; e.data.fd = w ->fd; if (w - >eventos == 0) op = EPOLL_CTL_ADD; senão operação = EPOLL_CTL_MOD; if (epoll_ctl(loop - >backend_fd, op, w - >fd, &e)) { if (errno! = EEXIST) abortar(); if (epoll_ctl(loop - >backend_fd, EPOLL_CTL_MOD, w - >fd, &e)) abortar(); } w - >eventos = w - >eventos; } para (;;) { for (i = 0; i < nfds; i++) { pe = eventos + i; fd = pe ->dados.fd; w = loop - >observadores[fd]; pe - >eventos &= w - >peventos | POLLERR | if (pe - >events == POLLERR || pe - >events == POLLHUP) pe - >events |= w - >pevents & (POLLIN | POLLOUT | UV__POLLRDHUP | UV__POLLPRI); if (pe - >eventos!= 0) { if (w == &loop - >signal_io_watcher) have_signals = 1; senão w - >cb(loop, w, pe - >events); neves++; } } if (have_signals! = 0) loop - >signal_io_watcher.cb(loop, &loop - >signal_io_watcher, POLLIN); }... }
No loop while, percorra a fila do observador watcher_queue, retire o evento e o descritor de arquivo e atribua-os ao objeto de evento e, e então chame a função epoll_ctl para registrar ou modificar o evento epoll.
No loop for, o descritor de arquivo que aguarda no epoll será retirado e atribuído ao nfds e, em seguida, o nfds será percorrido para executar a função de retorno de chamada.
uv__run_closing_handles
percorre a fila aguardando para ser fechada, fecha identificadores como stream, tcp, udp, etc. e, em seguida, chama o close_cb correspondente ao identificador. O código é o seguinte:
static void uv__run_closing_handles(uv_loop_t * loop) { uv_handle_t * p; uv_handle_t * q; p = loop ->closing_handles; loop ->closing_handles = NULL; enquanto (p) { q = p ->próximo_fechamento; uv__finish_close(p); p=q; } }
Embora process.nextTick e Promise sejam APIs assíncronas, elas não fazem parte da pesquisa de eventos. Elas têm suas próprias filas de tarefas, que são executadas após a conclusão de cada etapa da pesquisa de eventos. Portanto, quando usamos essas duas APIs assíncronas, precisamos prestar atenção. Se tarefas longas ou recursões forem executadas na função de retorno de chamada de entrada, a pesquisa de eventos será bloqueada, "morrendo de fome" as operações de E/S.
O código a seguir é um exemplo em que a função de retorno de chamada de fs.readFile não pode ser executada chamando prcoess.nextTick recursivamente.
fs.readFile('config.json', (erro, dados) = >{... }) const passagem = () = >{ process.nextTick(atravessar) }
Para resolver esse problema, você pode usar setImmediate, porque setImmediate executará a fila da função de retorno de chamada no loop de eventos. A fila de tarefas process.nextTick tem uma prioridade mais alta do que a fila de tarefas Promise. Por motivos específicos, consulte o seguinte código:
function processTicksAndRejections() {. deixe tocar; fazer { while (tock = fila.shift()) { const asyncId = tock[async_id_symbol]; emitBefore(asyncId, tock[trigger_async_id_symbol], tock); tentar { retorno de chamada const = tock.callback; if (tock.args === indefinido) { ligar de volta(); } outro { const args = tock.args; switch (argumentos.comprimento) { caso 1: retorno de chamada(args[0]); quebrar; caso 2: retorno de chamada(args[0], args[1]); quebrar; caso 3: retorno de chamada(args[0], args[1], args[2]); quebrar; caso 4: retorno de chamada(args[0], args[1], args[2], args[3]); quebrar; padrão: retorno de chamada(...args); } } } finalmente { if (destroyHooksExist()) emitDestroy(asyncId); } emitAfter(asyncId); } runMicrotasks(); } while (! fila . isEmpty () || processPromiseRejections()); setHasTickScheduled(falso); setHasRejectionToWarn(falso); }
Como pode ser visto na função processTicksAndRejections(), a função de retorno de chamada da fila é primeiro retirada por meio do loop while, e a função de retorno de chamada na fila da fila é adicionada por meio de process.nextTick. Quando o loop while termina, a função runMicrotasks() é chamada para executar a função de retorno de chamada Promise.
a estrutura central do Node.js que depende do libuv pode ser dividida em duas partes. Uma parte é a E/S da rede. A implementação subjacente dependerá de diferentes APIs de sistema, de acordo com os diferentes sistemas operacionais. /O, DNS e código do usuário Esta parte Use o pool de threads para processamento.
O mecanismo principal do libuv para lidar com operações assíncronas é a pesquisa de eventos. A pesquisa de eventos é dividida em várias etapas. A operação geral é percorrer e executar a função de retorno de chamada na fila.
Finalmente, é mencionado que a API assíncrona process.nextTick e Promise não pertencem à pesquisa de eventos. O uso inadequado causará o bloqueio da pesquisa de eventos.