JavaScript é uma linguagem de programação assíncrona (sem bloqueio) e de thread único, o que significa que apenas um processo pode ser executado por vez.
Em linguagens de programação, callback hell geralmente se refere a uma forma ineficaz de escrever código com chamadas assíncronas. Também é conhecida como Pirâmide da Perdição.
O inferno de retorno de chamada em JavaScript é conhecido como uma situação em que uma quantidade excessiva de funções de retorno de chamada aninhadas estão sendo executadas. Reduz a legibilidade e a manutenção do código. A situação de retorno de chamada normalmente ocorre ao lidar com operações de solicitação assíncronas, como fazer várias solicitações de API ou lidar com eventos com dependências complexas.
Para entender melhor o inferno dos retornos de chamada em JavaScript, primeiro entenda os retornos de chamada e os loops de eventos em JavaScript.
Retornos de chamada em JavaScript
JavaScript considera tudo como um objeto, como strings, arrays e funções. Conseqüentemente, o conceito de retorno de chamada nos permite passar a função como argumento para outra função. A função de retorno de chamada concluirá a execução primeiro e a função pai será executada mais tarde.
As funções de retorno de chamada são executadas de forma assíncrona e permitem que o código continue em execução sem esperar a conclusão da tarefa assíncrona. Quando múltiplas tarefas assíncronas são combinadas e cada tarefa depende da tarefa anterior, a estrutura do código torna-se complicada.
Vamos entender o uso e a importância dos callbacks. Vamos supor que temos uma função que aceita três parâmetros, uma string e dois números. Queremos alguma saída baseada no texto da string com múltiplas condições.
Considere o exemplo abaixo:
function expectedResult(action, x, y){ if(action === 'add'){ return x+y }else if(action === 'subtract'){ return x-y } } console.log(expectedResult('add',20,10)) console.log(expectedResult('subtract',30,10))
Saída:
30 20
O código acima funcionará bem, mas precisamos adicionar mais tarefas para tornar o código escalonável. O número de instruções condicionais também continuará aumentando, o que levará a uma estrutura de código confusa que precisa ser otimizada e legível.
Portanto, podemos reescrever o código de uma maneira melhor da seguinte forma:
function add(x,y){ return x+y } function subtract(x,y){ return x-y } function expectedResult(callBack, x, y){ return callBack(x,y) } console.log(expectedResult(add, 20, 10)) console.log(expectedResult(subtract, 30, 10))
Saída:
30 20
Ainda assim, a saída será a mesma. Mas no exemplo acima, definimos seu corpo de função separado e passamos a função como uma função de retorno de chamada para a função esperadoResult. Portanto, se quisermos estender a funcionalidade dos resultados esperados para que possamos criar outro corpo funcional com uma operação diferente e usá-lo como função de retorno de chamada, isso tornará mais fácil o entendimento e melhorará a legibilidade do código.
Existem outros exemplos diferentes de retornos de chamada disponíveis em recursos JavaScript suportados. Alguns exemplos comuns são ouvintes de eventos e funções de array, como mapear, reduzir, filtrar, etc.
Para entendê-lo melhor, devemos entender a passagem por valor e a passagem por referência do JavaScript.
JavaScript suporta dois tipos de dados que são primitivos e não primitivos. Os tipos de dados primitivos são indefinidos, nulos, string e booleanos, que não podem ser alterados, ou podemos dizer imutáveis comparativamente; tipos de dados não primitivos são arrays, funções e objetos que podem ser alterados ou mutáveis.
Passar por referência passa o endereço de referência de uma entidade, como uma função pode ser tomada como argumento. Portanto, se o valor dentro dessa função for alterado, o valor original será alterado, que está disponível fora da função.
Comparativamente, o conceito de passagem por valor não altera o seu valor original, que está disponível fora do corpo da função. Em vez disso, copiará o valor em dois locais diferentes usando sua memória. JavaScript identificou todos os objetos por sua referência.
Em JavaScript, o addEventListener escuta eventos como click, mouseover e mouseout e usa o segundo argumento como uma função que será executada assim que o evento for acionado. Esta função é usada como conceito de passagem por referência e passada sem parênteses.
Considere o exemplo abaixo; neste exemplo, passamos uma função greet como argumento para addEventListener como função de retorno de chamada. Ele será invocado quando o evento click for acionado:
Teste.html:
Javascript Callback Example <h3>Javascript Callback</h3> Click Here to Console const button = document.getElementById('btn'); const greet=()=>{ console.log('Hello, How are you?') } button.addEventListener('click', greet)
Saída:
No exemplo acima, passamos uma função greet como argumento para addEventListener como função de retorno de chamada. Ele será invocado quando o evento click for acionado.
Da mesma forma, o filtro também é um exemplo de função de retorno de chamada. Se usarmos um filtro para iterar um array, será necessária outra função de retorno de chamada como argumento para processar os dados do array. Considere o exemplo abaixo; neste exemplo, estamos usando a função maior para imprimir o número maior que 5 no array. Estamos usando a função isGreater como função de retorno de chamada no método filter.
const arr = [3,10,6,7] const isGreater = num => num > 5 console.log(arr.filter(isGreater))
Saída:
[ 10, 6, 7 ]
O exemplo acima mostra que a função maior é usada como função de retorno de chamada no método de filtro.
Para entender melhor os retornos de chamada e loops de eventos em JavaScript, vamos discutir JavaScript síncrono e assíncrono:
JavaScript síncrono
Vamos entender quais são os recursos de uma linguagem de programação síncrona. A programação síncrona possui os seguintes recursos:
Execução de bloqueio: A linguagem de programação síncrona suporta a técnica de execução de bloqueio, o que significa que bloqueia a execução de instruções subsequentes que as instruções existentes serão executadas. Assim, consegue a execução previsível e determinística das instruções.
Fluxo Sequencial: A programação síncrona suporta o fluxo sequencial de execução, o que significa que cada instrução é executada de forma sequencial, uma após a outra. O programa de linguagem aguarda a conclusão de uma instrução antes de passar para a próxima.
Simplicidade: Muitas vezes, a programação síncrona é considerada de fácil compreensão porque podemos prever sua ordem no fluxo de execução. Geralmente, é linear e fácil de prever. Os pequenos aplicativos são bons para serem desenvolvidos nessas linguagens porque podem lidar com a ordem crítica das operações.
Tratamento direto de erros: Em uma linguagem de programação síncrona, o tratamento de erros é muito fácil. Se ocorrer um erro quando uma instrução estiver sendo executada, ocorrerá um erro e o programa poderá detectá-lo.
Resumindo, a programação síncrona tem dois recursos principais, ou seja, uma única tarefa é executada por vez, e o próximo conjunto de tarefas seguintes só será abordado quando a tarefa atual for concluída. Assim, segue uma execução sequencial de código.
Esse comportamento da programação quando uma instrução está em execução, a linguagem cria uma situação de código de bloco, pois cada trabalho tem que aguardar a conclusão do trabalho anterior.
balanço java
Mas quando as pessoas falam sobre JavaScript, sempre houve uma resposta intrigante, seja ela síncrona ou assíncrona.
Nos exemplos discutidos acima, quando usamos uma função como retorno de chamada na função de filtro, ela foi executada de forma síncrona. Portanto, é chamada de execução síncrona. A função de filtro deve aguardar que a função maior termine sua execução.
Conseqüentemente, a função de retorno de chamada também é chamada de retorno de chamada de bloqueio, pois bloqueia a execução da função pai na qual foi invocada.
Principalmente, o JavaScript é considerado síncrono de thread único e de natureza bloqueadora. Mas usando algumas abordagens, podemos fazê-lo funcionar de forma assíncrona com base em diferentes cenários.
Agora, vamos entender o JavaScript assíncrono.
JavaScript assíncrono
A linguagem de programação assíncrona se concentra em melhorar o desempenho do aplicativo. Os retornos de chamada podem ser usados em tais cenários. Podemos analisar o comportamento assíncrono do JavaScript pelo exemplo abaixo:
function greet(){ console.log('greet after 1 second') } setTimeout(greet, 1000)
No exemplo acima, a função setTimeout recebe um retorno de chamada e o tempo em milissegundos como argumentos. O retorno de chamada é invocado após o tempo mencionado (aqui 1s). Resumindo, a função aguardará 1s para sua execução. Agora, dê uma olhada no código abaixo:
function greet(){ console.log('greet after 1 second') } setTimeout(greet, 1000) console.log('first') console.log('Second')
Saída:
first Second greet after 1 second
A partir do código acima, as mensagens de log após setTimeout serão executadas primeiro enquanto o cronômetro passa. Conseqüentemente, resulta um segundo e, em seguida, a mensagem de saudação após um intervalo de tempo de 1 segundo.
Em JavaScript, setTimeout é uma função assíncrona. Sempre que chamamos a função setTimeout, ela registra uma função de retorno de chamada (saudação neste caso) para ser executada após o atraso especificado. No entanto, não bloqueia a execução do código subsequente.
No exemplo acima, as mensagens de log são instruções síncronas executadas imediatamente. Eles não dependem da função setTimeout. Portanto, eles executam e registram suas respectivas mensagens no console sem esperar pelo atraso especificado em setTimeout.
Enquanto isso, o loop de eventos em JavaScript cuida das tarefas assíncronas. Neste caso, ele espera que o atraso especificado (1 segundo) passe e, após esse tempo, ele pega a função de retorno de chamada (saudação) e a executa.
Assim, o outro código após a função setTimeout estava sendo executado em segundo plano. Esse comportamento permite que o JavaScript execute outras tarefas enquanto aguarda a conclusão da operação assíncrona.
Precisamos entender a pilha de chamadas e a fila de retorno de chamada para lidar com eventos assíncronos em JavaScript.
Considere a imagem abaixo:
Na imagem acima, um mecanismo JavaScript típico consiste em uma memória heap e uma pilha de chamadas. A pilha de chamadas executa todo o código sem esperar quando é enviado para a pilha.
A memória heap é responsável por alocar memória para objetos e funções em tempo de execução sempre que necessário.
Agora, nossos mecanismos de navegador consistem em várias APIs da web, como DOM, setTimeout, console, fetch, etc., e o mecanismo pode acessar essas APIs usando o objeto de janela global. Na próxima etapa, alguns loops de eventos desempenham o papel de gatekeeper que seleciona solicitações de função dentro da fila de retorno de chamada e as envia para a pilha. Estas funções, como setTimeout, requerem um certo tempo de espera.
Agora vamos voltar ao nosso exemplo, a função setTimeout; quando a função é encontrada, o cronômetro é registrado na fila de retorno de chamada. Depois disso, o restante do código é colocado na pilha de chamadas e executado quando a função atinge o limite do temporizador, expira e a fila de retorno de chamada envia a função de retorno de chamada, que possui a lógica especificada e está registrada na função de tempo limite . Assim, será executado após o tempo especificado.
Cenários do Inferno de retorno de chamada
Agora, discutimos os retornos de chamada, síncronos, assíncronos e outros tópicos relevantes para o inferno dos retornos de chamada. Vamos entender o que é o inferno de retorno de chamada em JavaScript.
A situação em que vários retornos de chamada são aninhados é conhecida como inferno de retorno de chamada, pois seu formato de código se parece com uma pirâmide, também chamada de 'pirâmide da destruição'.
O inferno do retorno de chamada torna mais difícil entender e manter o código. Podemos ver essa situação principalmente enquanto trabalhamos no nó JS. Por exemplo, considere o exemplo abaixo:
getArticlesData(20, (articles) => { console.log('article lists', articles); getUserData(article.username, (name) => { console.log(name); getAddress(name, (item) => { console.log(item); //This goes on and on... } })
No exemplo acima, getUserData usa um nome de usuário que depende da lista de artigos ou precisa ser extraído da resposta getArticles que está dentro do artigo. getAddress também tem uma dependência semelhante, que depende da resposta do getUserData. Essa situação é chamada de inferno de retorno de chamada.
O funcionamento interno do callback hell pode ser entendido com o exemplo abaixo:
Vamos entender que precisamos realizar a tarefa A. Para realizar uma tarefa, precisamos de alguns dados da tarefa B. Da mesma forma; temos tarefas diferentes que dependem umas das outras e são executadas de forma assíncrona. Assim, ele cria uma série de funções de retorno de chamada.
Vamos entender as promessas em JavaScript e como elas criam operações assíncronas, permitindo-nos evitar escrever retornos de chamada aninhados.
Promessas de JavaScript
Em JavaScript, as promessas foram introduzidas no ES6. É um objeto com revestimento sintático. Devido ao seu comportamento assíncrono, é uma forma alternativa de evitar a gravação de retornos de chamada para operações assíncronas. Hoje em dia, APIs Web como fetch() são implementadas usando o promissor, que fornece uma forma eficiente de acessar os dados do servidor. Também melhorou a legibilidade do código e é uma forma de evitar a gravação de retornos de chamada aninhados.
As promessas na vida real expressam confiança entre duas ou mais pessoas e uma garantia de que algo específico certamente acontecerá. Em JavaScript, uma Promise é um objeto que garante a produção de um único valor no futuro (quando necessário). Promise em JavaScript é usado para gerenciar e lidar com operações assíncronas.
A Promessa retorna um objeto que garante e representa a conclusão ou falha de operações assíncronas e sua saída. É um proxy para um valor sem saber a saída exata. É útil para ações assíncronas fornecer um eventual valor de sucesso ou motivo de falha. Assim, os métodos assíncronos retornam os valores como um método síncrono.
Geralmente, as promessas têm os três estados a seguir:
array dinâmico em java
- Cumprido: O estado cumprido é quando uma ação aplicada foi resolvida ou concluída com êxito.
- Pendente: o estado Pendente é quando a solicitação está em andamento e a ação aplicada não foi resolvida nem rejeitada e ainda está em seu estado inicial.
- Rejeitado: O estado rejeitado é quando a ação aplicada foi rejeitada, causando falha na operação desejada. A causa da rejeição pode ser qualquer coisa, incluindo o servidor estar inativo.
A sintaxe para as promessas:
let newPromise = new Promise(function(resolve, reject) { // asynchronous call is made //Resolve or reject the data });
Abaixo está um exemplo de escrita das promessas:
Este é um exemplo de como escrever uma promessa.
function getArticleData(id) { return new Promise((resolve, reject) => { setTimeout(() => { console.log('Fetching data....'); resolve({ id: id, name: 'derik' }); }, 5000); }); } getArticleData('10').then(res=> console.log(res))
No exemplo acima, podemos ver como podemos usar as promessas de maneira eficiente para fazer uma solicitação do servidor. Podemos observar que a legibilidade do código é maior no código acima do que nos retornos de chamada. As promessas fornecem métodos como .then() e .catch(), que nos permitem lidar com o status da operação em caso de sucesso ou falha. Podemos especificar os casos para os diferentes estados das promessas.
Assíncrono/Aguarde em JavaScript
É outra maneira de evitar o uso de retornos de chamada aninhados. Async/Await nos permite usar as promessas com muito mais eficiência. Podemos evitar o uso do encadeamento de métodos .then() ou .catch(). Esses métodos também dependem das funções de retorno de chamada.
Async/Await pode ser usado com precisão com o Promise para melhorar o desempenho do aplicativo. Resolveu internamente as promessas e deu o resultado. Além disso, novamente, é mais legível que os métodos () ou catch().
Não podemos usar Async/Await com as funções normais de retorno de chamada. Para usá-lo, devemos tornar uma função assíncrona escrevendo uma palavra-chave async antes da palavra-chave function. Porém, internamente também utiliza encadeamento.
Abaixo está um exemplo de Async/Await:
async function displayData() { try { const articleData = await getArticle(10); const placeData = await getPlaces(article.name); const cityData = await getCity(place) console.log(city); } catch (err) { console.log('Error: ', err.message); } } displayData();
Para usar Async/Await a função deve ser especificada com a palavra-chave async, e a palavra-chave await deve ser escrita dentro da função. O assíncrono interromperá sua execução até que a promessa seja resolvida ou rejeitada. Será retomado quando a Promessa for distribuída. Uma vez resolvido, o valor da expressão await será armazenado na variável que a contém.
Resumo:
Resumindo, podemos evitar retornos de chamada aninhados usando promessas e async/await. Além dessas, podemos seguir outras abordagens, como escrever comentários, e dividir o código em componentes separados também pode ser útil. Mas, hoje em dia, os desenvolvedores preferem o uso de async/await.
Conclusão:
O inferno de retorno de chamada em JavaScript é conhecido como uma situação em que uma quantidade excessiva de funções de retorno de chamada aninhadas estão sendo executadas. Reduz a legibilidade e a manutenção do código. A situação de retorno de chamada normalmente ocorre ao lidar com operações de solicitação assíncronas, como fazer várias solicitações de API ou lidar com eventos com dependências complexas.
Para entender melhor o inferno do retorno de chamada em JavaScript.
JavaScript considera tudo como um objeto, como strings, arrays e funções. Conseqüentemente, o conceito de retorno de chamada nos permite passar a função como argumento para outra função. A função de retorno de chamada concluirá a execução primeiro e a função pai será executada mais tarde.
As funções de retorno de chamada são executadas de forma assíncrona e permitem que o código continue em execução sem esperar a conclusão da tarefa assíncrona.