JavaScript assíncrono: do inferno de retorno de chamada ao assíncrono e à espera

Publicados: 2022-03-11

Uma das chaves para escrever uma aplicação web bem-sucedida é poder fazer dezenas de chamadas AJAX por página.

Este é um desafio típico de programação assíncrona, e como você escolhe lidar com chamadas assíncronas, em grande parte, fará ou quebrará seu aplicativo e, por extensão, potencialmente toda a sua inicialização.

A sincronização de tarefas assíncronas em JavaScript foi um problema sério por muito tempo.

Esse desafio está afetando tanto os desenvolvedores de back-end que usam Node.js quanto os desenvolvedores de front-end que usam qualquer estrutura JavaScript. A programação assíncrona faz parte do nosso trabalho diário, mas o desafio é muitas vezes encarado de forma leve e não considerado no momento certo.

Uma Breve História do JavaScript Assíncrono

A primeira e mais direta solução veio na forma de funções aninhadas como retornos de chamada . Essa solução levou a algo chamado callback hell , e muitos aplicativos ainda sofrem com isso.

Então, nós temos Promessas . Esse padrão tornou o código muito mais fácil de ler, mas estava muito longe do princípio Don't Repeat Yourself (DRY). Ainda havia muitos casos em que você precisava repetir os mesmos trechos de código para gerenciar adequadamente o fluxo do aplicativo. A última adição, na forma de instruções async/await, finalmente tornou o código assíncrono em JavaScript tão fácil de ler e escrever quanto qualquer outro pedaço de código.

Vamos dar uma olhada nos exemplos de cada uma dessas soluções e refletir sobre a evolução da programação assíncrona em JavaScript.

Para fazer isso, examinaremos uma tarefa simples que executa as seguintes etapas:

  1. Verifique o nome de usuário e a senha de um usuário.
  2. Obtenha funções de aplicativo para o usuário.
  3. Registre o tempo de acesso do aplicativo para o usuário.

Abordagem 1: Callback Hell (“A Pirâmide da Perdição”)

A solução antiga para sincronizar essas chamadas era por meio de retornos de chamada aninhados. Essa era uma abordagem decente para tarefas JavaScript assíncronas simples, mas não seria dimensionada devido a um problema chamado callback hell.

Ilustração: antipadrão do inferno de retorno de chamada JavaScript assíncrono

O código para as três tarefas simples seria algo assim:

 const verifyUser = function(username, password, callback){ dataBase.verifyUser(username, password, (error, userInfo) => { if (error) { callback(error) }else{ dataBase.getRoles(username, (error, roles) => { if (error){ callback(error) }else { dataBase.logAccess(username, (error) => { if (error){ callback(error); }else{ callback(null, userInfo, roles); } }) } }) } }) };

Cada função recebe um argumento que é outra função que é chamada com um parâmetro que é a resposta da ação anterior.

Muitas pessoas experimentarão o congelamento do cérebro apenas lendo a frase acima. Ter um aplicativo com centenas de blocos de código semelhantes causará ainda mais problemas para a pessoa que mantém o código, mesmo que ela mesma o tenha escrito.

Este exemplo fica ainda mais complicado quando você percebe que um database.getRoles é outra função que possui retornos de chamada aninhados.

 const getRoles = function (username, callback){ database.connect((connection) => { connection.query('get roles sql', (result) => { callback(null, result); }) }); };

Além de ter um código difícil de manter, o princípio DRY não tem absolutamente nenhum valor nesse caso. O tratamento de erros, por exemplo, é repetido em cada função e o retorno de chamada principal é chamado de cada função aninhada.

Operações JavaScript assíncronas mais complexas, como fazer loop por chamadas assíncronas, são um desafio ainda maior. Na verdade, não há uma maneira trivial de fazer isso com retornos de chamada. É por isso que bibliotecas JavaScript Promise como Bluebird e Q ganharam tanta força. Eles fornecem uma maneira de realizar operações comuns em solicitações assíncronas que a própria linguagem ainda não fornece.

É aí que entram as promessas nativas de JavaScript.

Promessas de JavaScript

Promessas eram o próximo passo lógico para escapar do inferno do callback. Esse método não eliminou o uso de callbacks, mas facilitou o encadeamento de funções e simplificou o código, tornando-o muito mais fácil de ler.

Ilustração: diagrama de promessas de JavaScript assíncrono

Com Promises em vigor, o código em nosso exemplo de JavaScript assíncrono ficaria assim:

 const verifyUser = function(username, password) { database.verifyUser(username, password) .then(userInfo => dataBase.getRoles(userInfo)) .then(rolesInfo => dataBase.logAccess(rolesInfo)) .then(finalResult => { //do whatever the 'callback' would do }) .catch((err) => { //do whatever the error handler needs }); };

Para alcançar esse tipo de simplicidade, todas as funções usadas no exemplo teriam que ser Promisified . Vamos dar uma olhada em como o método getRoles seria atualizado para retornar um Promise :

 const getRoles = function (username){ return new Promise((resolve, reject) => { database.connect((connection) => { connection.query('get roles sql', (result) => { resolve(result); }) }); }); };

Modificamos o método para retornar um Promise , com dois callbacks, e o próprio Promise executa ações do método. Agora, os retornos de chamada de resolve e reject serão mapeados para os métodos Promise.then e Promise.catch , respectivamente.

Você pode notar que o método getRoles ainda é internamente propenso ao fenômeno da pirâmide da destruição. Isso se deve à forma como os métodos de banco de dados são criados, pois eles não retornam Promise . Se nossos métodos de acesso ao banco de dados também retornassem Promise , o método getRoles seria semelhante ao seguinte:

 const getRoles = new function (userInfo) { return new Promise((resolve, reject) => { database.connect() .then((connection) => connection.query('get roles sql')) .then((result) => resolve(result)) .catch(reject) }); };

Abordagem 3: Async/Aguardar

A pirâmide da desgraça foi significativamente mitigada com a introdução de Promises. No entanto, ainda tínhamos que contar com retornos de chamada que são passados ​​para os métodos .catch .then um Promise .

As promessas abriram o caminho para uma das melhorias mais legais em JavaScript. O ECMAScript 2017 trouxe açúcar sintático em cima de Promises em JavaScript na forma de instruções async e await .

Eles nos permitem escrever código baseado em Promise como se fosse síncrono, mas sem bloquear o thread principal, como demonstra este exemplo de código:

 const verifyUser = async function(username, password){ try { const userInfo = await dataBase.verifyUser(username, password); const rolesInfo = await dataBase.getRoles(userInfo); const logStatus = await dataBase.logAccess(userInfo); return userInfo; }catch (e){ //handle errors as needed } };

Awaiting Promise to solve é permitido apenas em funções async , o que significa que verifyUser teve que ser definido usando a async function .

No entanto, uma vez que essa pequena alteração é feita, você pode await qualquer Promise sem alterações adicionais em outros métodos.

Assíncrono - Uma resolução há muito esperada de uma promessa

As funções assíncronas são o próximo passo lógico na evolução da programação assíncrona em JavaScript. Eles tornarão seu código muito mais limpo e fácil de manter. Declarar uma função como async garantirá que ela sempre retorne uma Promise para que você não precise mais se preocupar com isso.

Por que você deve começar a usar a função async JavaScript hoje?

  1. O código resultante é muito mais limpo.
  2. O tratamento de erros é muito mais simples e depende de try / catch como em qualquer outro código síncrono.
  3. A depuração é muito mais simples. Definir um ponto de interrupção dentro de um bloco .then não moverá para o próximo .then porque ele apenas percorre o código síncrono. Mas, você pode percorrer as chamadas de await como se fossem chamadas síncronas.