Guia para iniciantes em JavaScript assíncrono

javascript
promessas
AsyncAwait
Guia para iniciantes em JavaScript assíncrono cover image

Se você está apenas começando com programação, é provável que esteja pensando em programas como um conjunto de blocos sequenciais de lógica, onde cada bloco faz uma coisa específica e passa seu resultado para que o próximo bloco possa ser executado e assim por diante, e para o na maioria das vezes você está certo, a maioria dos programas são executados de maneira sequencial. Este modelo nos permite construir programas que são simples de escrever e manter. No entanto, existem casos de uso específicos em que esse modelo sequencial não funcionaria ou não seria o ideal. Como exemplo, considere um aplicativo leitor de livros. Este aplicativo possui alguns recursos avançados, como encontrar todas as ocorrências de uma palavra, navegar entre marcadores e similares. Agora imagine que o usuário esteja lendo um livro longo e decida procurar todas as ocorrências de uma palavra comum como “O”. O aplicativo normalmente leva alguns segundos para localizar e indexar todas as ocorrências dessa palavra. Em um programa sequencial, o usuário não pode interagir com a aplicação (mudar de página ou destacar um texto) até que a operação de busca seja cumprida. Esperançosamente, você verá que essa não é uma experiência de usuário ideal!

1

O diagrama ilustra um fluxo de execução típico do aplicativo leitor de livros. Se o usuário iniciar uma operação de longa duração (neste caso, a busca por todas as ocorrências de “o” em um livro grande), o aplicativo “congela” durante toda a duração dessa operação. Neste caso, o usuário continuará clicando no próximo botão de favorito sem nenhum resultado até que a operação de pesquisa seja concluída e todas as operações terão efeito de uma só vez, dando ao usuário final a sensação de um aplicativo lento.

Você deve ter notado que este exemplo não corresponde realmente ao modelo sequencial que apresentamos anteriormente. Isso ocorre porque as operações aqui são independentes umas das outras. O usuário não precisa saber o número de ocorrências de “o” para navegar para o próximo marcador, portanto a ordem de execução das operações não é muito importante. Não precisamos esperar o final da operação de pesquisa para podermos navegar para o próximo marcador. Uma possível melhoria no fluxo de execução anterior é baseada nesta lógica: podemos executar a operação de pesquisa longa em segundo plano, prosseguir com quaisquer operações recebidas e, uma vez concluída a operação longa, podemos simplesmente notificar o usuário. O fluxo de execução fica da seguinte forma:

2

Com esse fluxo de execução, a experiência do usuário é significativamente melhorada. Agora o usuário pode iniciar uma operação de longa duração, continuar usando o aplicativo normalmente e ser notificado assim que a operação for concluída. Esta é a base da programação assíncrona.

Javascript, entre outras linguagens, suporta esse estilo de programação assíncrona, fornecendo APIs extensas para alcançar praticamente qualquer comportamento assíncrono que você possa imaginar. No final das contas, Javascript deve ser inerentemente uma linguagem assíncrona. Se nos referirmos ao exemplo anterior, a lógica assíncrona está na base de todos os aplicativos de interação do usuário, e o Javascript foi construído principalmente para ser usado no navegador, onde a maioria dos programas trata de responder às ações do usuário.

A seguir, você encontrará um breve guia sobre Javascript assíncrono:

Retornos de chamada

Em um programa típico, você normalmente encontrará diversas funções. Para usar uma função, nós a chamamos com um conjunto de parâmetros. O código da função será executado e retornará um resultado, nada fora do comum. A programação assíncrona muda ligeiramente essa lógica. Voltando ao exemplo do aplicativo leitor de livros, não podemos usar uma função regular para implementar a lógica da operação de pesquisa, pois a operação leva um tempo desconhecido. Uma função regular retornará basicamente antes que a operação seja concluída, e este não é o comportamento que esperamos. A solução é especificar outra função que será executada assim que a operação de busca for concluída. Isso modela nosso caso de uso, pois nosso programa pode continuar seu fluxo normalmente e assim que a operação de pesquisa for concluída, a função especificada será executada para notificar o usuário sobre os resultados da pesquisa. Esta função é o que chamamos de função de retorno de chamada:

// Search occurrences function
function searchOccurrences(word, callback) {
  try {
    // search operation logic, result is in result variable
    //....
    callback(null, word, result);
  } catch (err) {
    callback(err);
  }
}

// Search occurrences callback function
function handleSearchOccurrencesResult(err, word, result) {
  if (err) {
    console.log(`Search operation for ${word} ended with an error`);
  } else console.log(`Search results for ${word}: ${result}`);
  return;
}

searchOccurrences("the", handleSearchOccurrencesResult);

Primeiro, definimos a função de operação de pesquisa, searchOccurrences. É necessária a palavra a ser buscada e um segundo parâmetro “callback” que será a função a ser executada assim que a operação de busca for concluída. A função de operação de busca foi intencionalmente mantida abstrata, só precisamos focar em seus dois resultados possíveis: o primeiro caso é onde tudo deu certo e temos o resultado da busca na variável resultado. Neste caso, basta chamar a função de retorno de chamada com os seguintes parâmetros: o primeiro parâmetro é nulo, o que significa que nenhum erro ocorreu, o segundo parâmetro é a palavra que foi pesquisada e o terceiro e talvez o mais importante parâmetro dos três., é o resultado da operação de pesquisa.

O segundo caso é onde ocorre um erro, este também é o caso onde a execução da operação de busca é feita e temos que chamar a função de retorno de chamada. Usamos um bloco try e catch para interceptar qualquer erro e apenas chamamos a função de retorno de chamada com o objeto de erro do bloco catch.

Em seguida, definimos a função de retorno de chamada, handleSearchOccurrences, e mantivemos sua lógica bastante simples. É apenas uma questão de imprimir uma mensagem no console. Primeiro verificamos o parâmetro “err” para ver se ocorreu algum erro na função principal. Nesse caso, apenas informamos ao usuário que a operação de pesquisa terminou com erro. Caso nenhum erro tenha sido gerado, imprimimos uma mensagem com o resultado da operação de busca.

Por fim, chamamos a função searchOccurrences com a palavra “o”. A função agora será executada normalmente sem bloquear o programa principal e assim que a pesquisa for concluída, o callback será executado e obteremos a mensagem de resultado com o resultado da pesquisa ou com a mensagem de erro.

É importante mencionar aqui que só temos acesso à variável result dentro das funções main e callback. Se tentarmos algo assim:

let result;
function searchOccurrences(word, callback) {
  try {
    // search operation logic, result is in searchResult variable
    //....
    result = searchResult;
    callback(null, word, result);
  } catch (err) {
    callback(err);
  }
}
searchOccurrences("the", handleSearchOccurrencesResult);
console.log(result);

o resultado da impressão seria indefinido porque os programas não aguardam a execução da função searchOccurrences. Ele passa para a próxima instrução, que é a instrução print antes que a variável de resultado seja atribuída dentro da função principal. Como resultado, imprimiremos a variável de resultado não atribuída.

Portanto, com base nessa lógica, devemos manter todo o código que usa a variável result dentro da função de retorno de chamada. Isso pode não parecer um problema agora, mas pode rapidamente se transformar em um problema real. Imagine o caso em que temos uma cadeia de funções assíncronas que precisam ser executadas em sequência. Na lógica típica de retorno de chamada, você implementaria algo assim:

functionA(function (err, resA) {
  ///......
  functionB(resA, function (err, resB) {
    ///......
    functionC(resB, function (err, resC) {
      ///......
      functionD(resC, function (err, resD) {
        ///......
      });
    });
  });
});

Lembre-se de que cada retorno de chamada possui um parâmetro de erro e cada erro deve ser tratado separadamente. Isso torna o já complexo código acima ainda mais complexo e difícil de manter. Esperançosamente, as promessas estão aqui para resolver o problema do inferno de retorno de chamada, abordaremos isso a seguir.

Promessas

As promessas são construídas com base em retornos de chamada e operam de maneira semelhante. Eles foram introduzidos como parte dos recursos do ES6 para resolver alguns dos problemas gritantes com retornos de chamada, como o inferno de retorno de chamada. As promessas fornecem suas próprias funções que são executadas quando concluídas com êxito (resolução) e quando ocorrem erros (rejeição). O exemplo a seguir mostra o exemplo searchOccurrences implementado com promessas:

// Search occurrences function
function searchOccurrences(word) {
  return new Promise((resolve, reject) => {
    try {
      // search operation logic, result is in result variable
      //....
      resolve(word, result);
    } catch (err) {
      reject(err);
    }
  });
}

searchOccurrences("the")
  .then((word, result) => {
    console.log(`Search results for ${word}: ${result}`);
  })
  .catch((err) => {
    console.log(`Search operation ended with an error`);
  });

Vejamos as alterações que aplicamos:

A função searchOccurrences retorna uma promessa. Dentro da promessa, mantemos a mesma lógica: temos duas funções resolve e rejeitar que representam nossos retornos de chamada em vez de ter uma única função de retorno de chamada que lida com uma execução bem-sucedida e uma execução com erros. As promessas separam os dois resultados e fornecem uma sintaxe limpa ao chamar a função principal. A função resolve é “conectada” à função principal usando a palavra-chave “then”. Aqui apenas especificamos os dois parâmetros da função de resolução e imprimimos o resultado da pesquisa. Uma coisa semelhante se aplica à função de rejeição, ela pode ser conectada usando a palavra-chave “catch”. Esperamos que você possa apreciar as vantagens que as promessas oferecem em termos de legibilidade e limpeza do código. Se você ainda está debatendo isso, veja como podemos resolver o problema do callback hell encadeando as funções assíncronas para serem executadas uma após a outra:

searchOccurrences("the")
  .then(searchOccurrences("asynchronous"))
  .then(searchOccurrences("javascript"))
  .then(searchOccurrences("guide"))
  .catch((err) => {
    console.log(`Search operation ended with an error`);
  });

Assíncrono/Aguarda

Async/Await é a mais recente adição ao nosso conjunto de ferramentas assíncronas em Javascript. Introduzidos no ES8, eles fornecem uma nova camada de abstração sobre funções assíncronas, simplesmente “esperando” pela execução de uma operação assíncrona. O fluxo do programa é bloqueado nessa instrução até que um resultado da operação assíncrona seja retornado e então o programa prosseguirá com a próxima instrução. Se você está pensando no fluxo de execução síncrona, você está correto. Nós temos um círculo completo! Async/await tenta trazer a simplicidade da programação síncrona para o mundo assíncrono. Tenha em mente que isso só é percebido na execução e no código do programa. Tudo permanece igual, Async/await ainda usa promessas e retornos de chamada são seus blocos de construção.

Vamos repassar nosso exemplo e implementá-lo usando Async/await:

async function searchOccurrences(word) {
  try {
    // search operation logic, result is in result variable
    //....
    return result;
  } catch (err) {
    console.log(`Search operation for ${word} ended with an error`);
  }
}

const word = "the";

const result = await searchOccurrences(word, handleSearchOccurrencesResult);

console.log(`Search results for ${word}: ${result}`);

Nosso código não mudou muito, o importante a notar aqui é a palavra-chave “async” antes da declaração da função searchOccurrences. Isso indica que a função é assíncrona. Além disso, observe a palavra-chave “await” ao chamar a função searchOccurrences. Isso instruirá o programa a aguardar a execução da função até que o resultado seja retornado antes que o programa possa passar para a próxima instrução, ou seja, a variável result sempre conterá o valor retornado da função searchOccurrences e não a promessa de a função, nesse sentido, Async/Await não possui um estado pendente como Promises. Terminada a execução, passamos para a instrução print e desta vez o resultado realmente contém o resultado da operação de pesquisa. Como esperado, o novo código tem o mesmo comportamento como se fosse síncrono.

Outra coisa importante a ter em mente é que, como não temos mais funções de retorno de chamada, precisamos tratar o erro searchOccurrences dentro da mesma função, pois não podemos simplesmente propagar o erro para a função de retorno de chamada e tratá-lo lá. Aqui estamos apenas imprimindo uma mensagem de erro em caso de erro para fins de exemplo.

Embrulhar

Neste artigo examinamos as diferentes abordagens usadas para implementar lógica assíncrona em Javascript. Começamos explorando um exemplo concreto de por que precisaríamos mudar do estilo de programação síncrona regular para o modelo assíncrono. Em seguida, passamos para os retornos de chamada, que são os principais blocos de construção do Javascript assíncrono. As limitações dos callbacks nos levaram às diferentes alternativas que foram adicionadas ao longo dos anos para superar essas limitações, principalmente promessas e Async/await. A lógica assíncrona pode ser encontrada em qualquer lugar na web, esteja você chamando uma API externa, iniciando uma consulta ao banco de dados, gravando no sistema de arquivos local ou até mesmo aguardando a entrada do usuário em um formulário de login. Esperançosamente, agora você se sente mais confiante para resolver esses problemas escrevendo Javascript assíncrono limpo e de fácil manutenção!

Se você gostou deste artigo, confira o Blog do CLA onde discutimos diversos assuntos sobre como entrar na área de tecnologia. Além disso, confira nosso canal do YouTube para ver nossos workshops gratuitos anteriores e siga-nos nas redes sociais para não perder os próximos!


Career Services background pattern

Serviços de carreira

Contact Section background image

Vamos manter-nos em contacto

Code Labs Academy © 2024 Todos os direitos reservados.