Artigo original: Node.js: what it is, when and how to use it, and why you should

Escrito por: Pablo Regen

Você, provavelmente, já leu essas frases antes…

O Node.js é um ambiente de tempo de execução de JavaScript criado usando a engine de JavaScript do Chrome, a V8
O Node.js usa um modelo assíncrono e não bloqueador da E/S, orientado a eventos
O Node.js opera em um loop de eventos em thread única

Se já, você pode ter se perguntado o significado de tudo isso. Ao final deste artigo, espero que você tenha uma compreensão melhor dessas frases e saiba o que o Node é, como ele funciona e por que e quando é uma boa ideia usá-lo.

Vamos começar examinando a terminologia.

E/S (entrada e saída – em inglês I/O, ou input/output)

E/S é a abreviação para (dispositivos de) entrada/saída. E/S (ou I/O) tem a ver primordialmente com a interação do programa com o disco e com a rede do sistema. Exemplos de operações de E/S incluem a leitura/gravação de dados de/para uma unidade de disco, a realização de solicitações de HTTP e a comunicação com um banco de dados. São dispositivos muito lentos se comparados com o acesso à memória (RAM) ou com a realização de tarefas pela CPU.

Síncrono x Assíncrono

Execução síncrona (ou "sync") geralmente se refere ao código sendo executado em sequência. Na programação síncrona, o programa é executado linha a linha, uma linha de cada vez. Sempre que uma função é chamada, a execução do programa aguarda até que aquela função retorne um resultado antes de continuar na próxima linha do código.

Execução assíncrona (ou "async") é a execução que não é feita na mesma sequência em que aparece no código. Na programação assíncrona, o programa não aguarda até que a tarefa seja concluída, podendo seguir para a próxima tarefa.

No exemplo a seguir, a operação síncrona faz com que os alertas sejam disparados em sequência. Na operação assíncrona, embora alert(2) pareça ser o segundo a ser executado, não é isso que acontece.

// Operação síncrona: 1,2,3
alert(1);
alert(2);
alert(3);

// Operação assíncrona: 1,3,2
alert(1);
setTimeout(() => alert(2), 0);
alert(3);

Uma operação assíncrona é frequentemente relacionada a E/S, embora setTimeout seja um exemplo de algo que não tenha a ver com E/S, mas ainda seja assíncrono. De um modo geral, qualquer coisa relacionada à computação é sincronizada e qualquer coisa relacionada à entrada/saída/tempo é assíncrona. A razão para as operações de E/S serem feitas de maneira assíncrona é que elas são muito lentas e bloqueariam a execução adicional do código do contrário.

Bloqueador x não bloqueador

O modelo bloqueador se refere a operações que bloqueiam/impedem o resto da execução até que aquela operação esteja concluída. Não bloqueador, por sua vez, se refere ao código que não impede o resto da execução. Conforme vemos na documentação do Node.js, operações bloqueadoras ocorrem quando a execução de JavaScript adicional no processo do Node.js deve aguardar até que uma operação não relacionada ao JavaScript seja concluída.

Métodos bloqueadores executam de modo síncrono, enquanto métodos não bloqueadores executam de modo assíncrono.

// Bloqueador
const fs = require('fs');
const dados = fs.readFileSync('/arquivo.md'); // há um bloqueio aqui até que o arquivo seja lido
console.log(dados);
maisTarefas(); // será executado após o console.log

// Não bloqueador
const fs = require('fs');
fs.readFile('/arquivo.md', (err, dados) => {
  if (err) throw err;
  console.log(dados);
});
maisTarefas(); // será executado antes do console.log

No primeiro exemplo, console.log será chamado antes de maisTarefas(). No segundo exemplo, fs.readFile() é não bloqueador, o que faz com que a execução do JavaScript possa continuar e chamar maisTarefas() primeiro.

No Node, não bloqueador tem primordialmente a ver com operações de E/S e com JavaScript de baixo desempenho em função de demandar demais da CPU, enquanto aguardar por uma operação que não seja do JavaScript, como as operações de E/S, não é tipicamente considerado bloqueador.

Todos os métodos de E/S na biblioteca padrão do Node.js fornecem versões assíncronas – e, portanto, não bloqueadoras – e que aceitam funções de callback. Alguns métodos também têm equivalentes bloqueadores, cujos nomes terminam em "Sync" (veja os exemplos de readFile e readFileSync acima).

Operações de E/S não bloqueadores permitem que um único processo sirva diversas solicitações ao mesmo tempo. Em vez de o processo ser bloqueado e ficar esperando que operações de E/S sejam concluídas, as operações de E/S são delegadas ao sistema. Assim, o processo pode executar a parte seguinte do código. Operações de E/S não bloqueadoras fornecem uma função de callback, que é chamada quando a operação é concluída.

Callbacks

Callbacks são funções passadas como argumento de uma outra função, e que, então, podem ser invocadas (chamadas novamente, ou, em inglês, called back) dentro da função externa para concluir algum tipo de ação na hora em que for conveniente. A chamada pode ser imediata (callback síncrona) ou pode acontecer mais tarde (callback assíncrona).

// Callback síncrona
function saudacao(callback) {
  callback();
}
saudacao(() => { console.log('Olá'); });
maisTarefas(); // será executado após o console.log

// Callback assíncrona
const fs = require('fs');
fs.readFile('/arquivo.md', function callback(err, dados) { // fs.readFile é um método assíncrono fornecido pelo Node
  if (err) throw err;
  console.log(dados);
});
maisTarefas(); // será executado antes do console.log

No primeiro exemplo, a função de callback é chamada imediatamente dentro da função externa saudacao e faz o registro no console antes que maisTarefas() seja executada.

No segundo exemplo, fs.readFile (um método assíncrono fornecido pelo Node) lê o arquivo e, quando termina, chama a função de callback com um erro ou o conteúdo do arquivo. Enquanto isso, o programa pode continuar a execução do código.

Uma função de callback assíncrona pode ser chamada quando um evento acontece ou quando uma tarefa é concluída. Ela evita o bloqueio, permitindo que outro código seja executado enquanto isso.

Em vez de o código ser lido de cima para baixo de modo procedural, os programas assíncronos podem executar funções diferentes em momentos diferentes com base na ordem e velocidade em que funções anteriores, como solicitações de http ou leituras do sistema de arquivos, acontecem. Eles são usados quando você não sabe quando alguma operação assíncrona será concluída.

É preciso, no entanto, evitar o chamado "inferno das callbacks", situação na qual as callbacks são aninhadas dentro de outras callbacks por vários níveis, tornando o código de difícil compreensão, além de difícil de manter e depurar.

Eventos e programação orientada a eventos

Eventos são ações geradas pelo usuário ou pelo sistema, como um clique, um download de arquivo concluído ou um erro de hardware ou software.

A programação orientada a eventos é um paradigma de programação no qual o fluxo do programa é determinado por eventos. Um programa orientado a eventos executa ações em resposta a eventos. Quando ocorre um evento, ele dispara uma função de callback.

Agora, vamos tentar entender o Node e ver como tudo isso se relaciona com ele.

Node.js: o que é, por que foi criado e como funciona?

Colocado de maneira simples, o Node.js é uma plataforma que executa programas em JavaScript do lado do servidor que pode se comunicar com fontes de E/S, como redes e arquivos de sistemas.

Quando Ryan Dahl criou o Node em 2009, ele disse que a E/S vinha sendo manipulada incorretamente, bloqueando todo o processo devido à programação síncrona.

Técnicas de servir conteúdo para a web tradicionais usavam o modelo de threads, ou seja, usavam uma thread para cada solicitação. Como, em uma operação de E/S, a solicitação passa a maior parte do tempo aguardando até ser concluída, cenários com uso intenso de E/S incorporam uma grande quantidade de recursos não utilizados (como a memória) associados a essas threads. Portanto, o modelo de "uma thread por solicitação" para um servidor é um modelo que não dimensiona muito bem.

Dahl argumentou que o software deveria ser capaz de realizar várias tarefas e propôs eliminar o tempo gasto aguardando que os resultados de E/S retornassem. Em vez do modelo de threads, ele disse que a maneira certa de lidar com várias conexões simultâneas era ter uma thread única, um loop de eventos e E/S sem bloqueio. Por exemplo, quando você faz uma consulta a um banco de dados, em vez de aguardar a resposta, você dá a ele uma função de callback para que sua execução possa rodar essa instrução e continuar fazendo outras coisas. Quando os resultados retornam, você pode executar a callback.

O loop de eventos é o que permite que o Node.js execute operações de E/S sem bloqueio, apesar do fato de que o JavaScript é de thread única. O loop, que é executado na mesma thread que o código em JavaScript, pega uma tarefa do código e a executa. Se a tarefa for assíncrona ou uma operação de E/S, o loop a delegará para o kernel do sistema, como no caso de novas conexões com o servidor, ou para um pool de threads, como operações relacionadas ao sistema de arquivos. O loop, então, pega a próxima tarefa e a executa.

Como a maioria dos kernels modernos têm várias threads, eles podem lidar com várias operações em execução em segundo plano. Quando uma dessas operações é concluída (este é um evento), o kernel informa a Node.js para que a callback apropriada (aquela que dependia da conclusão da operação) possa ser adicionada à fila para, por fim, ser executada.

O nó controla as operações assíncronas inacabadas, enquanto o loop de eventos continua em loop para verificar se elas estão concluídas até que todas elas estejam.

Para acomodar o loop de eventos de thread única, o Node.js usa a biblioteca libuv, que, por sua vez, usa um pool de threads de tamanho fixo que lida com a execução de algumas das operações de E/S assíncronas não bloqueadoras em paralelo. As funções de chamada da thread principal lançam tarefas na fila de tarefas compartilhadas, que as threads no pool de threads resgatam e executam.

Funções do sistema inerentemente sem bloqueio, como a rede, são convertidas em soquetes sem bloqueio do lado do kernel, enquanto funções do sistema inerentemente bloqueadas, como E/S de arquivos, são executadas de maneira bloqueada em suas próprias threads. Quando uma thread no pool de threads conclui uma tarefa, ela informa a thread principal sobre isso, que, por sua vez, ativa e executa a callback registrada.

1_pIEFRBvMqxpDipMnqkVprA
Imagem da apresentação de Phillip Roberts na JSConf EU: What the heck is the event loop anyway?

A imagem acima é retirada da apresentação de Philip Roberts na JSConf EU: What the heck is the event loop anyway? ("O que diabos é esse o loop de eventos?", em português). Recomendo assistir o vídeo inteiro (em inglês) para se ter uma ideia geral de como funciona o loop de eventos.

O diagrama explica como o loop de eventos funciona com o navegador, mas sua aparência é basicamente idêntica à do Node. Em vez de APIs da Web, teríamos APIs do Node.

De acordo com a apresentação, a pilha de chamadas (também conhecida como pilha de execução, "call stack" ou, simplesmente, "a stack") é uma estrutura de dados que registra onde no programa estamos. Se entrarmos em uma função, colocamos algo na pilha. Se retornarmos de uma função, nós a tiramos do topo da pilha.

É assim que o código no diagrama é processado quando o executamos:

  1. Colocamos main() na pilha (o próprio arquivo)
  2. Colocamos console.log('Hi'); na pilha, que é executado imediatamente, registrando "Hi" ("Olá") no console e sendo removido da pilha
  3. Colocamos setTimeout(cb, 5000) na pila. setTimeout é uma API fornecida pelo navegador (no back-end, seria uma API do Node). Quando setTimeout é chamada com a função de callback e com os argumentos de atraso, o navegador aciona um temporizador com o tempo de atraso (no caso, 5 mil milissegundos)
  4. A chamada de setTimeout é concluída e ela é removida da pilha
  5. Colocamos console.log(‘JSConfEU’); na pilha, que executa imediatamente, registrando "JSConfEU" no console e é removida da pilha
  6. main() é removida da pilha
  7. Após 5 mil milissegundos, o temporizador da API conclui e a função de callback é movida para a fila de tarefas
  8. O loop de eventos verifica se a pilha está vazia, já que o JavaScript, por ser de thread única, só consegue fazer uma coisa por vez (setTimeout não é garantido, apenas um tempo mínimo para a execução). Se a pilha estiver vazia, ele pega a primeira coisa que estiver na fila e a envia para a pilha. Assim, o loop coloca a callback na pilha
  9. A callback é executada, registra "there" (no final, temos a expressão "Hi there", algo como "Olá, pessoal!", registrado) no console e é removida da pilha. Isso encerra o processo

Se quiser se aprofundar ainda mais nos detalhes de como o Node.js, a libuv, o loop de eventos e o pool de threads funcionam, sugiro conferir os recursos na seção se referências do final – em especial esta, esta e esta referências, juntamente com a documentação do Node (texto e vídeos em inglês).

1_GYkdiL25aLDgkSW0phpAag
O loop de eventos. Imagem da apresentação de Bert Belder: Everything You Need to Know About Node.js Event Loop

Node.js: por que e onde usá-lo?

Como quase nenhuma função no Node executa diretamente a E/S, o processo nunca bloqueia (as operações de E/S são descarregadas e executadas de maneira assíncrona no sistema), tornando-se uma boa opção para desenvolver sistemas altamente escaláveis.

Devido ao seu loop de eventos orientado a eventos, thread única e modelo de E/S assíncrono sem bloqueio, o Node.js tem melhor desempenho em aplicações de uso intenso de E/S que exigem velocidade e escalabilidade com muitas conexões simultâneas, como streaming de vídeo e áudio, aplicações em tempo real, bate-papos ao vivo, aplicações de jogos, ferramentas de colaboração ou software de bolsa de valores.

O Node.js pode não ser a escolha certa para operações que exigem muito da CPU. Em vez disso, o modelo de thread tradicional pode ter um desempenho melhor.

npm

1_Qj1OTPHk-djj2C1Nnkn4VQ

O npm é o gerenciador de pacotes padrão do Node.js e é instalado no sistema quando o Node.js é instalado. Ele pode gerenciar pacotes que são dependências locais de um projeto específico, bem como ferramentas do JavaScript instaladas de modo global.

O site www.npmjs.com hospeda milhares de bibliotecas gratuitas para baixar e usar em seu programa para tornar o desenvolvimento mais rápido e eficiente. No entanto, como qualquer pessoa pode criar bibliotecas e não há processo de verificação para o envio, você deve ter cuidado com as de baixa qualidade, inseguras ou maliciosas. O npm depende de relatórios de usuários para remover pacotes se eles violarem as políticas e, para ajudar você a decidir, inclui estatísticas como número de downloads e número de pacotes dependentes.

Como executar código no Node.js

Comece instalando o Node no seu computador, se você ainda não o tiver. A maneira mais fácil é visitar nodejs.org e clicar no botão para fazer o download. A menos que você queira ou precise ter acesso aos recursos mais recentes, baixe a versão LTS (Long Term Support – em português, suporte de longa duração) para seu sistema operacional.

Você executa uma aplicação do Node a partir do terminal do seu computador. Por exemplo, faça um arquivo chamado "app.js" e adicione console.log('Olá'); a ele. No seu terminal, mude o diretório para a pasta onde se encontra o arquivo e execute o comando node app.js. Você verá "Olá" no console. 🙂

Referências

Aqui temos alguns recursos interessantes que eu revisei durante a composição deste artigo.

Apresentações do Node.js feitas pelo criador (em inglês):

Apresentações sobre o Node, o loop de eventos e a biblioteca libuv (em inglês):

Documentação do Node (em inglês):

Recursos adicionais (em inglês, exceto os recursos da Wikipédia):

Obrigado pela leitura.