Como funciona o Node.js

Concluído

Esta unidade explica como o Node.js lida com tarefas de entrada para o runtime do JavaScript.

Tipos de tarefas

Os aplicativos JavaScript têm dois tipos de tarefas:

  • Tarefas síncronas: Essas tarefas ocorrem em ordem. Elas não dependem de outro recurso para serem concluídas. Exemplos são operações matemáticas ou manipulação de cadeia de caracteres.
  • Assíncrono: Essas tarefas podem não ser concluídas imediatamente porque dependem de outros recursos. Exemplos são solicitações de rede ou operações do sistema de arquivos.

Como você deseja que seu programa seja executado o mais rápido possível, você deseja que o mecanismo JavaScript possa continuar funcionando enquanto aguarda uma resposta de uma operação assíncrona. Para fazer isso, ele adiciona a tarefa assíncrona a uma fila de tarefas e continua trabalhando na próxima tarefa.

Gerenciar fila de tarefas com loop de evento

O Node.js usa a arquitetura orientada a eventos do mecanismo JavaScript para processar solicitações assíncronas. O diagrama a seguir ilustra como o loop de eventos V8 funciona em um alto nível:

Diagram showing how Node J S uses an event-driven architecture where an event loop processes events and returns callbacks.

Uma tarefa assíncrona, indicada pela sintaxe apropriada (mostrada abaixo), é adicionada ao loop de eventos. A tarefa inclui o trabalho a ser feito e uma função de retorno de chamada para receber os resultados. Quando a operação intensiva é concluída, a função de retorno de chamada é disparada com os resultados.

Operações síncronas versus operações assíncronas

As APIs Node.js fornecem operações assíncronas e síncronas para algumas das mesmas operações, como operações de arquivo. Embora geralmente você sempre deva priorizar assíncrono, há momentos em que você pode usar operações síncronas.

Um exemplo é quando uma interface de linha de comando (CLI) lê um arquivo e, em seguida, usa imediatamente os dados no arquivo. Nesse caso, você pode usar a versão síncrona da operação de arquivo porque não há nenhum outro sistema ou pessoa aguardando para usar o aplicativo.

No entanto, se você estiver criando um servidor Web, sempre deverá usar a versão assíncrona da operação de arquivo para não bloquear a capacidade de execução do thread único para processar outras solicitações de usuário.

Em seu trabalho como desenvolvedor da TailWind Traders, você precisará entender a diferença entre operações síncronas e assíncronas e quando usar cada uma delas.

Desempenho por meio de operações assíncronas

O Node.js aproveita a natureza exclusiva orientada a eventos do JavaScript, que torna a composição de tarefas de servidor rápida e de alto desempenho. O JavaScript, quando usado corretamente com técnicas assíncronas, pode produzir os mesmos resultados de desempenho que linguagens de baixo nível, como C, devido a aumentos de desempenho possibilitados pelo mecanismo V8.

As técnicas assíncronas vêm em três estilos, que você precisa ser capaz de reconhecer em seu trabalho:

  • Assíncrono/espera (recomendado): A técnica assíncrona mais recente que usa as palavras-chave async e await para receber os resultados de uma operação assíncrona. Assíncrono/espera é usado em várias linguagens de programação. Geralmente, novos projetos com dependências mais recentes usarão esse estilo de código assíncrono.
  • Retornos de chamada: A técnica assíncrona original que usa uma função de retorno de chamada para receber os resultados de uma operação assíncrona. Você verá isso em bases de código mais antigas e em APIs Node.js mais antigas.
  • Promessas: Uma técnica assíncrona mais recente que usa um objeto promise para receber os resultados de uma operação assíncrona. Você verá isso em bases de código mais recentes e em APIs Node.js mais recentes. Talvez seja necessário escrever código baseado em promessas em seu trabalho para encapsular APIs mais antigas que não serão atualizadas. Usando promessas para esse encapsulamento, você permite que o código seja usado em um intervalo maior de projetos com versão no Node.js do que no estilo de código assíncrono/espera mais recente.

Assíncrono/espera

Assíncrono/espera é uma maneira mais recente de lidar com programação assíncrona. Assíncrono/espera é açúcar sintático em cima de promessas, e faz com que o código assíncrono pareça mais com código síncrono. Também é mais fácil de ler e manter.

O mesmo exemplo usando assíncrono/espera tem esta aparência:

// async/await asynchronous example

const fs = require('fs').promises;

const filePath = './file.txt';

// `async` before the parent function
async function readFileAsync() {
  try {
    // `await` before the async method
    const data = await fs.readFile(filePath, 'utf-8');
    console.log(data);
    console.log('Done!');
  } catch (error) {
    console.log('An error occurred...: ', error);
  }
}

readFileAsync()
  .then(() => {
    console.log('Success!');
  })
  .catch((error) => {
    console.log('An error occurred...: ', error);
  });

Quando assíncrono/espera foi lançado no ES2017, as palavras-chave só podiam ser usadas em funções cuja função de nível superior fosse uma promessa. Embora a promessa não precisasse ter e seções then e catch, ainda era necessário ter sintaxe promise para ser executada.

Uma função async sempre retorna uma promessa, mesmo que não tenha uma chamada await dentro dela. A promessa será resolvida com o valor devolvido pela função. Se a função lançar um erro, a promessa será rejeitada com o valor lançado.

Promises

Como os retornos de chamada aninhados podem ser difíceis de ler e gerenciar, o Node.js adicionou suporte para promessas. Uma promessa é um objeto que representa a conclusão (ou falha) eventual de uma operação assíncrona.

Uma função de promessa tem o formato de:

// Create a basic promise function
function promiseFunction() {
  return new Promise((resolve, reject) => {
    // do something

    if (error) {
      // indicate success
      reject(error);
    } else {
      // indicate error
      resolve(data);
    }
  });
}

// Call a basic promise function
promiseFunction()
  .then((data) => {
    // handle success
  })
  .catch((error) => {
    // handle error
  });

O método then é chamado quando a promessa é cumprida e o método catch é chamado quando a promessa é rejeitada.

Para ler um arquivo de forma assíncrona com promessas, o código é:

// promises asynchronous example

const fs = require('fs').promises;
const filePath = './file.txt';

// request to read a file
fs.readFile(filePath, 'utf-8')
  .then((data) => {
    console.log(data);
    console.log('Done!');
  })
  .catch((error) => {
    console.log('An error occurred...: ', error);
  });

console.log(`I'm the last line of the file!`);

Assíncrono/espera de nível superior

As versões mais recentes do Node.js adicionaram assíncrono/espera de nível superior para módulos ES6. Você precisa adicionar uma propriedade denominada type no package.json com um valor de module para usar esse recurso.

{
    "type": "module"
}

Em seguida, você pode usar a palavra-chave await no nível superior do código

// top-level async/await asynchronous example

const fs = require('fs').promises;

const filePath = './file.txt';

// `async` before the parent function
try {
  // `await` before the async method
  const data = await fs.readFile(filePath, 'utf-8');
  console.log(data);
  console.log('Done!');
} catch (error) {
  console.log('An error occurred...: ', error);
}
console.log("I'm the last line of the file!");

Retornos de chamada

Quando o Node.js foi originalmente lançado, a programação assíncrona era tratada usando funções de retorno de chamada. Retornos de chamada são funções que são passadas como argumentos para outras funções. Quando a tarefa é concluída, a função de retorno de chamada é chamada.

A ordem dos parâmetros da função é importante. A função de retorno de chamada é o último parâmetro da função.

// Callback function is the last parameter
function(param1, param2, paramN, callback)

O nome da função no código que você mantém pode não ser denominado callback. Ele pode ser denominado cb ou done ou next. O nome da função não é importante, mas a ordem dos parâmetros é importante.

Observe que não há nenhuma indicação sintática de que a função é assíncrona. Você precisa saber que a função é assíncrona lendo a documentação ou continuando a ler o código.

Exemplo de retorno de chamada com função de retorno de chamada nomeada

O código a seguir separa a função assíncrona do retorno de chamada. Isso é fácil de ler e entender e permite reutilizar o retorno de chamada para outras funções assíncronas.

// callback asynchronous example

// file system module from Node.js
const fs = require('fs');

// relative path to file
const filePath = './file.txt';

// callback
const callback = (error, data) => {
  if (error) {
    console.log('An error occurred...: ', error);
  } else {
    console.log(data); // Hi, developers!
    console.log('Done!');
  }
};

// async request to read a file
//
// parameter 1: filePath
// parameter 2: encoding of utf-8
// parmeter 3: callback function
fs.readFile(filePath, 'utf-8', callback);

console.log("I'm the last line of the file!");

O resultado correto é:

I'm the last line of the file!
Hi, developers!
Done!

Primeiro, a função assíncrona fs.readFile é iniciada e entra no loop de eventos. Em seguida, a execução do código continua para a próxima linha de código, que é a última console.log. Depois que o arquivo é lido, a função de retorno de chamada é chamada e as duas instruções console.log são executadas.

Exemplo de retorno de chamada com função anônima

O exemplo a seguir usa uma função de retorno de chamada anônima, o que significa que a função não tem um nome e não pode ser reutilizada por outras funções anônimas.

// callback asynchronous example

// file system module from Node.js
const fs = require('fs');

// relative path to file
const filePath = './file.txt';

// async request to read a file
//
// parameter 1: filePath
// parameter 2: encoding of utf-8
// parmeter 3: callback function () => {}
fs.readFile(filePath, 'utf-8', (error, data) => {
  if (error) {
    console.log('An error occurred...: ', error);
  } else {
    console.log(data); // Hi, developers!
    console.log('Done!');
  }
});

console.log("I'm the last line of the file!");

O resultado correto é:

I'm the last line of the file!
Hi, developers!
Done!

Quando o código é executado, a função assíncrona fs.readFile é iniciada e entra no loop de eventos. Em seguida, a execução continua para a linha de código a seguir, que é a última console.log. Quando o arquivo é lido, a função de retorno de chamada é chamada e as duas instruções console.log são executadas.

Retornos de chamada aninhados

Como talvez seja necessário chamar um retorno de chamada assíncrono subsequente e, em seguida, outro, o código de retorno de chamada pode ficar aninhado. Isso é chamado de inferno de retorno de chamada e é difícil de ler e manter.

// nested callback example

// file system module from Node.js
const fs = require('fs');

fs.readFile(param1, param2, (error, data) => {
  if (!error) {
    fs.writeFile(paramsWrite, (error, data) => {
      if (!error) {
        fs.readFile(paramsRead, (error, data) => {
          if (!error) {
            // do something
          }
        });
      }
    });
  }
});

APIs síncronas

O Node.js também tem um conjunto de APIs síncronas. Essas APIs bloqueiam a execução do programa até que a tarefa seja concluída. APIs síncronas são úteis quando você deseja ler um arquivo e, em seguida, usar imediatamente os dados no arquivo.

Funções síncronas (bloqueio) no Node.js usam a convenção de nomenclatura de functionSync. Por exemplo, a API assíncrona readFile tem um equivalente síncrono chamado readFileSync. É importante manter esse padrão em seus próprios projetos para que seu código seja fácil de ler e entender.

// synchronous example

const fs = require('fs');

const filePath = './file.txt';

try {
  // request to read a file
  const data = fs.readFileSync(filePath, 'utf-8');
  console.log(data);
  console.log('Done!');
} catch (error) {
  console.log('An error occurred...: ', error);
}

Como um novo desenvolvedor na TailWind Traders, você pode ser solicitado a modificar qualquer tipo de código Node.js. É importante entender a diferença entre APIs síncronas e assíncronas e as diferentes sintaxes para código assíncrono.