JavaScript - Assincronismo

nascimento_

Jorge Nascimento

Posted on February 17, 2023

JavaScript - Assincronismo

Normalmente, o código de um programa é executado de forma direta, com uma coisa acontecendo por vez. Se uma função depende do resultado de outra função, ela tem que esperar o retorno do resultado, e até que isso aconteça, o programa inteiro praticamente para de funcionar do ponto de vista do usuário.

Dependendo do ambiente de programação que você está usando (navegadores da Web, no caso de desenvolvimento da Web) esses fornecem API's ( fetch, promise, works) que permitem executar essas tarefas de forma assíncrona, ou seja, realizar processamento fora da thread principal e após finalizar o resultado é retornado.

Técnicas async ( assíncronas ) são muito úteis, principalmente na programação web. Quando um aplicativo web é executado em um navegador e executa um pedaço de código rigoroso sem retornar o controle para o navegador, ele pode parecer que travou. Isso é chamado de blocking; o navegador está bloqueado de continuar a manusear a entrada do usuário e de realizar outras tarefas até que o aplicativo web retorne o controle do processador.

Tarefas pesadas como cálculos demorados, ou buscar uma informação em servidor podem demorar e bloqueiam o navegador e isso torna uma experiência ruim para o usuário.

Threads

Uma thread é basicamente um único processo que um programa pode usar para concluir tarefas. Cada thread só pode fazer uma tarefa de cada vez:

async

Cada tarefa será executada sequencialmente, uma tarefa tem que ser concluída antes que a próxima possa ser iniciada.

JavaScript é tradicionalmente single-threaded. Mesmo com múltiplos núcleos de processamento, você só pode fazê-lo executar tarefas em uma única thread, chamada de main thread (thread principal).

Depois de um tempo, o JavaScript ganhou algumas ferramentas para ajudar em tais problemas. As Web workers te permitem mandar parte do processamento do JavaScript para uma thread separada.

Você geralmente usaria uma worker para executar um processo pesado para que a UI não seja bloqueada.

thread

Main thread: Tarefa A --> Tarefa C

Worker thread: Tarefa pesada B

Código Assíncrono

Os Works ajudam no processamento paralelo, porém eles possuem limitações e não são a principal solução em JavaScript para lidar com assíncronismo. Os browsers nos permitem executar certas operações de modo assíncrono através de recursos como Promises que te permitem executar uma operação e depois esperar pelo resultado antes de executar outra operação:

Main thread: Tarefa A                   Tarefa B
Promise:         | ---> operação async ---> |
Enter fullscreen mode Exit fullscreen mode

Já que a operação está acontecendo em outro lugar, a main thread não está bloqueada enquanto a operação assíncrona está sendo processada.

JavaScript Async

A programação assíncrona é uma técnica que permite que seu programa inicie uma tarefa potencialmente de longa duração e ainda seja capaz de responder a outros eventos enquanto essa tarefa é executada, em vez de ter que esperar até que essa tarefa seja concluída.

Muitas funções fornecidas pelos navegadores, podem levar muito tempo e, portanto, são assíncronas. Por exemplo:

  • Fazendo solicitações HTTP usando fetch()
  • Acessar a camera ou o microfone de um usuário usando getUserMedia()
  • Pedindo a um usuário para selecionar arquivos usando showOpenFilePicker()

Na programação síncrona o programa executa o código uma linha de cada vez na ordem em que o escrevemos, aguardando a finalização da tarefa anterior para seguir para a próxima.

O problema dessa abordagem é que, quando o navegador precisa lidar com processamento que são potencialmente demorados a página trava até que o processamento retorne com o resultado.

O conceito de assíncronismo é usada para resolver esse tipo de problema, pois podemos delegar um processamento para uma função e continuar o fluxo de execução, após o processamento for concluído o seu retorno fica disponível para utilização.

  • Manipuladores de eventos são exemplos de uso de assíncronismo, pois recebem funções ( callbacks ) que são executadas baseada em disparo de evento.

Funções de Callback

Callbacks são funções passado como argumentos de outras funções, que é chamada de volta quando um determinado evento/processo seja acionado, como por exemplo um evento do navegador como click .

Essa era forma mais utilizada para lidar com comportamento assíncrono no passado e que ainda é utilizado por algumas API's event-driven. Dependendo da operação a utilização de callback pode deixa o código com difícil legibilidade e dificulta manutenção conforme o exemplo abaixo:

function doStep1(init, callback) {
  const result = init + 1;
  callback(result);
}

function doStep2(init, callback) {
  const result = init + 2;
  callback(result);
}

function doStep3(init, callback) {
  const result = init + 3;
  callback(result);
}

function doOperation() {
  doStep1(0, (result1) => {
    doStep2(result1, (result2) => {
      doStep3(result2, (result3) => {
        console.log(`result: ${result3}`);
      });
    });
  });
}

console.log(doOperation()); // => result: 6
Enter fullscreen mode Exit fullscreen mode

Como temos que chamar callbacks dentro de callbacks, obtemos uma função doOperation() profundamente aninhada ( às vezes é chamado de callback hell ), que é muito mais difícil de ler e depurar, como forma de lidar com esse problema foram criado novas formas de lidar com assíncronismo como Promise.

Promises

Uma Promise é um objeto que representa a eventual conclusão ou falha de uma operação assíncrona e é a forma mais moderna de lidar com processamento assíncrono em javascript.

Para utilizar uma Promise é necessário instanciar la com o operador new passando uma função que recebe 2 argumentos ( resolve, reject ), esses são funções que são chamadas em caso de sucesso ou falha da operação.

Uma Promise sempre retorna outra promise e utilizamos os métodos .then e .catch que retorna resultado seja de sucesso ou falha como parâmetro em sua função de callback.

Vamos refatorar o exemplo anterior para melhor entendimento:

function doStep1(init) {
  return new Promise((resolve, reject) => {
    const result = init + 1;

    resolve(result);

    // tratar possíveis erros - reject(erro)
  });
}

function doStep2(init) {
  return new Promise((resolve, reject) => {
    const result = init + 2;

    resolve(result);

    // tratar possíveis erros - reject(erro)
  });
}

function doStep3(init) {
  return new Promise((resolve, reject) => {
    const result = init + 3;

    resolve(result);

    // tratar possíveis erros - reject(erro)
  });
}

function doOperation() {
  return doStep1(0)
    .then((result1) => doStep2(result1))
    .then((result2) => doStep3(result2))
    .then((result3) => console.log(`result: ${result3}`))
    .catch((err) => console.log(err));
}

doOperation(); // => result: 6
Enter fullscreen mode Exit fullscreen mode

Percebemos que removemos a função de callback e capturamos o valor do processamento através do método .then, esse pode ser encadeado com outras chamadas conforme o exemplo ( evitando o callback hell ).

Utilizamos o .catch ao final para capturar possíveis erros no processamento. ( Nesse exemplo não foi simulado processamento assíncrono e retorno de erro por se tratar de um exemplo simplificado ).

Promises deixam o código mais legível, fácil de entender e manter em casos mais complexos.

A API de Promise possui métodos que ajudam em execução de operação de forma paralela como o Promise.all(), Promise.race() entre outros, para maior conhecimento deixarei o link do documentação ao final do post.

Async / Await

Async / Await é um sintax sugar ( forma simplificada ) para utilizar promises, com essa sintaxe o código fica similar a um código síncrono e não é necessário realizar encadeamento de métodos.

Normalmente utilizamos await dentro de uma função com o prefixo async, o await aguarda a resolução de uma Promise que geralmente é armazenada em uma variável e utilizada conforme desejado na sequencia do código.

Utilizamos o bloco try catch para captura possíveis erros nos retorno das promises.

Vamos refatorar o mesmo exemplo anterior:

function doStep1(init) {
  return new Promise((resolve, reject) => {
    const result = init + 1;

    resolve(result);

    // tratar possíveis erros - reject(erro)
  });
}

function doStep2(init) {
  return new Promise((resolve, reject) => {
    const result = init + 2;

    resolve(result);

    // tratar possíveis erros - reject(erro)
  });
}

function doStep3(init) {
  return new Promise((resolve, reject) => {
    const result = init + 3;

    resolve(result);

    // tratar possíveis erros - reject(erro)
  });
}
async function doOperation() {
  try {
    const result1 = await doStep1(0);
    const result2 = await doStep2(result1);
    const result3 = await doStep3(result2);

    console.log(`result: ${result3}`);
  } catch (err) {
    console.log(err);
  }
}

doOperation(); // => result: 6
Enter fullscreen mode Exit fullscreen mode

Basicamente a única alteração é na forma de como tratamos o retorno de uma promise, precisamos de uma função async e dentro dessa função utilizamos o await para aguardar retornos de promises e tratar conforme o desejado, deixando um código mais linear e evitando excesso de complexidade.

Então é isso pessoal, nesse artigo conseguimos ter uma noção do que é assíncronismo e como a linguagem Javascript lida com esse comportamento para evitar bloqueio da sua thread, outro assunto importante para entender por menores internos da linguagem é estudar o Event Loop que será tema do próximo post.

Me paga um ☕ ? | pix: nascimento.dev.io@gmail.com

Este post tem como objetivo ajudar quem esta começando no aprendizado das tecnologias web, além de servir como incentivo no meus estudos e a criar outros posts pra fixação do aprendizado.

Me Sigam :)

Linkedin | Github

💖 💪 🙅 🚩
nascimento_
Jorge Nascimento

Posted on February 17, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related