Como funciona o Node.js

Concluído

Esta unidade explica como Node.js lida com tarefas de entrada para o tempo de execução do JavaScript.

Tipos de tarefas

As aplicações JavaScript têm dois tipos de tarefas:

  • Tarefas síncronas: essas tarefas acontecem em ordem. Eles não dependem de outro recurso para serem concluídos. Exemplos são operações matemáticas ou manipulação de strings.
  • Assíncronas: 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 eventos

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:

Diagrama mostrando como o Nó J S usa uma arquitetura orientada a eventos onde um loop de eventos processa eventos e retorna retornos de chamada.

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 é acionada 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ê deva sempre pensar assíncrono primeiro, 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 do arquivo porque não há outro sistema ou pessoa esperando para usar o aplicativo.

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

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

Desempenho através de operações assíncronas

Node.js aproveita a natureza exclusiva orientada a eventos do JavaScript que torna a composição de tarefas do 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 aos aumentos de desempenho possibilitados pelo mecanismo V8.

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

  • Async/await (recomendado): A mais nova técnica assíncrona que usa as palavras-chave e await para receber os resultados de uma operação assíncronaasync. Async/await é usado em muitas 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 promessa 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 Node.js do que no estilo de código async/await mais recente.

Sincronizar/aguardar

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

O mesmo exemplo usando async/await 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 async/await foi lançado no ES2017, as palavras-chave só podiam ser usadas em funções com a função de nível superior sendo uma promessa. Embora a promessa não precisasse ter then e catch seções, ainda era necessário ter promise sintaxe para ser executada.

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

Promessas

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

Uma função promise 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 then método é chamado quando a promessa é cumprida e o catch método é 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!`);

Async/await de nível superior

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

{
    "type": "module"
}

Em seguida, você pode usar a await palavra-chave no nível superior do seu 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!");

Chamadas de retorno

Quando Node.js foi lançado originalmente, 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 chamado callbackde . Pode chamar-se cb ou nextdone . 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ê tem que 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 que você reutilize 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 fs.readFile assíncrona é 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 fs.readFile assíncrona é iniciada e entra no loop de eventos. Em seguida, a execução continua para a seguinte linha de código, 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 você pode precisar 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

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. As APIs síncronas são úteis quando você deseja ler um arquivo e, em seguida, usar imediatamente os dados no arquivo.

As funções síncronas (bloqueio) no Node.js usam a convenção de nomenclatura do functionSync. Por exemplo, a API assíncrona readFile tem uma contraparte síncrona chamada 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.