O autor selecionou o Fundo de Auxílio COVID-19 para receber uma doação como parte do programa Escreva para Doações.
Introdução
Nos primeiros dias da internet, os sites frequentemente consistiam em dados estáticos em uma página HTML. Mas agora que as aplicações web se tornaram mais interativas e dinâmicas, tem se tornado cada vez mais necessário realizar operações intensivas como fazer solicitações de rede externa para recuperar dados de APIs. Para lidar com essas operações em JavaScript, um desenvolvedor deve usar técnicas de programação assíncrona.
Como o JavaScript é uma linguagem de programação single-threaded com um modelo de execução síncrono que processa uma operação após a outra, ele só pode processar uma instrução por vez. No entanto, uma ação como solicitar dados de uma API pode levar um tempo indeterminado, dependendo do tamanho dos dados solicitados, da velocidade da conexão de rede e de outros fatores. Se as chamadas de API fossem realizadas de maneira síncrona, o navegador não seria capaz de lidar com nenhuma entrada do usuário, como rolar ou clicar em um botão, até que essa operação fosse concluída. Isso é conhecido como bloqueio.
Para evitar o comportamento de bloqueio, o ambiente do navegador possui muitas APIs da Web às quais o JavaScript pode acessar de forma assíncrona, o que significa que elas podem ser executadas em paralelo com outras operações em vez de sequencialmente. Isso é útil porque permite que o usuário continue usando o navegador normalmente enquanto as operações assíncronas estão sendo processadas.
Como desenvolvedor JavaScript, você precisa saber como trabalhar com APIs da Web assíncronas e lidar com a resposta ou erro dessas operações. Neste artigo, você aprenderá sobre o loop de eventos, a forma original de lidar com o comportamento assíncrono por meio de callbacks, a adição atualizada de promessas do ECMAScript 2015 e a prática moderna de usar async/await.
Observação: Este artigo está focado em JavaScript do lado do cliente no ambiente do navegador. Os mesmos conceitos geralmente são verdadeiros no ambiente Node.js, no entanto, o Node.js usa suas próprias APIs em C++ em vez das APIs da Web do navegador. Para obter mais informações sobre programação assíncrona no Node.js, confira Como Escrever Código Assíncrono no Node.js.
O Loop de Eventos
Esta seção explicará como o JavaScript lida com código assíncrono usando o event loop. Primeiro, ele passará por uma demonstração do event loop em ação e, em seguida, explicará os dois elementos do event loop: a pilha e a fila.
O código JavaScript que não usa nenhuma API da Web assíncrona será executado de forma síncrona — um de cada vez, sequencialmente. Isso é demonstrado pelo código de exemplo a seguir, que chama três funções, cada uma imprimindo um número no console:
Neste código, você define três funções que imprimem números com console.log()
.
Em seguida, faça chamadas para as funções:
A saída será baseada na ordem em que as funções foram chamadas — first()
, second()
, depois third()
:
Output1
2
3
Quando uma API da Web assíncrona é usada, as regras se tornam mais complicadas. Uma API integrada que você pode testar isso é setTimeout
, que define um temporizador e executa uma ação após um período de tempo especificado. setTimeout
precisa ser assíncrono, caso contrário, o navegador inteiro ficaria congelado durante a espera, o que resultaria em uma experiência do usuário ruim.
Adicione setTimeout
à função second
para simular uma solicitação assíncrona:
setTimeout
recebe dois argumentos: a função que será executada de forma assíncrona e a quantidade de tempo que esperará antes de chamar essa função. Neste código, você encapsulou console.log
em uma função anônima e a passou para setTimeout
, em seguida, definiu a função para ser executada após 0
milissegundos.
Agora, chame as funções, como fez anteriormente:
Você pode esperar que, com um setTimeout
definido para 0
milissegundos, a execução dessas três funções ainda resulte nos números sendo impressos em ordem sequencial. Mas porque é assíncrono, a função com o tempo de espera será impressa por último:
Output1
3
2
Independentemente de você definir o tempo de espera para zero segundos ou cinco minutos, não fará diferença – o console.log
chamado pelo código assíncrono será executado após as funções síncronas de nível superior. Isso ocorre porque o ambiente hospedeiro JavaScript, neste caso o navegador, usa um conceito chamado de loop de eventos para lidar com concorrência ou eventos paralelos. Como o JavaScript só pode executar uma instrução de cada vez, ele precisa que o loop de eventos seja informado de quando executar qual instrução específica. O loop de eventos lida com isso com os conceitos de uma pilha e uma fila.
Pilha
A pilha, ou pilha de chamadas, mantém o estado de qual função está atualmente em execução. Se você não está familiarizado com o conceito de uma pilha, pode imaginá-la como um array com propriedades de “Último a entrar, primeiro a sair” (LIFO), o que significa que você só pode adicionar ou remover itens do final da pilha. O JavaScript executará o quadro atual (ou chamada de função em um ambiente específico) na pilha e, em seguida, o removerá e passará para o próximo.
Para o exemplo que contém apenas código síncrono, o navegador manipula a execução na seguinte ordem:
- Adicionar
primeiro()
à pilha, executarprimeiro()
que registra1
no console, removerprimeiro()
da pilha. - Adicionar
segundo()
à pilha, executarsegundo()
que registra2
no console, removersegundo()
da pilha. - Adicionar
terceiro()
à pilha, executarterceiro()
que registra3
no console, removerterceiro()
da pilha.
O segundo exemplo com setTimeout
parece assim:
- Adicionar
primeiro()
à pilha, executarprimeiro()
que registra1
no console, removerprimeiro()
da pilha. - Adicionar
segundo()
à pilha, executarsegundo()
.- Adicionar
setTimeout()
à pilha, executar osetTimeout()
Web API que inicia um temporizador e adiciona a função anônima à fila, removersetTimeout()
da pilha.
- Adicionar
- Remova
second()
da pilha. - Adicione
third()
à pilha, executethird()
que registra3
no console, removathird()
da pilha. - O event loop verifica a fila em busca de mensagens pendentes e encontra a função anônima de
setTimeout()
, adiciona a função à pilha que registra2
no console, em seguida, remove-a da pilha.
O uso de setTimeout
, uma API Web assíncrona, introduz o conceito da fila, que este tutorial abordará a seguir.
Fila
A fila, também referida como fila de mensagens ou fila de tarefas, é uma área de espera para funções. Sempre que a pilha de chamadas estiver vazia, o event loop verificará a fila em busca de mensagens esperando, começando pela mensagem mais antiga. Quando encontrar uma, adicionará à pilha, que executará a função na mensagem.
No exemplo de setTimeout
, a função anônima é executada imediatamente após o restante da execução no nível superior, uma vez que o temporizador foi definido para 0
segundos. É importante lembrar que o temporizador não significa que o código será executado exatamente em 0
segundos ou no tempo especificado, mas sim que adicionará a função anônima à fila naquele período de tempo. Esse sistema de fila existe porque se o temporizador adicionasse a função anônima diretamente à pilha quando o temporizador terminasse, isso interromperia qualquer função que estivesse sendo executada no momento, o que poderia ter efeitos indesejados e imprevisíveis.
Nota: Existe também outra fila chamada fila de trabalhos ou fila de microtarefas que lida com promessas. Microtarefas como promessas são tratadas com prioridade mais alta do que macrotarefas como setTimeout
.
Agora você sabe como o loop de eventos usa a pilha e a fila para lidar com a ordem de execução do código. A próxima tarefa é descobrir como controlar a ordem de execução no seu código. Para fazer isso, você aprenderá primeiro sobre a maneira original de garantir que o código assíncrono seja tratado corretamente pelo loop de eventos: funções de retorno de chamada.
Funções de Retorno de Chamada
No exemplo de setTimeout
, a função com o tempo de espera foi executada após tudo no contexto de execução principal de nível superior. Mas se você quisesse garantir que uma das funções, como a função terceira
, fosse executada após o tempo limite, então você teria que usar métodos de codificação assíncrona. O tempo limite aqui pode representar uma chamada de API assíncrona que contém dados. Você deseja trabalhar com os dados da chamada de API, mas precisa garantir que os dados sejam retornados primeiro.
A solução original para lidar com esse problema é usar funções de retorno de chamada. As funções de retorno de chamada não têm uma sintaxe especial; elas são apenas uma função que foi passada como argumento para outra função. A função que recebe outra função como argumento é chamada de função de ordem superior. De acordo com esta definição, qualquer função pode se tornar uma função de retorno de chamada se for passada como argumento. As funções de retorno de chamada não são assíncronas por natureza, mas podem ser usadas para fins assíncronos.
Aqui está um exemplo de código sintático de uma função de ordem superior e uma função de retorno de chamada:
No código, você define uma função fn
, define uma função funcaoDeOrdemSuperior
que recebe uma função retornoDeChamada
como argumento e passa fn
como retorno de chamada para funcaoDeOrdemSuperior
.
Ao executar este código, você obterá o seguinte:
OutputJust a function
Vamos voltar para as funções first
, second
e third
com setTimeout
. Até agora, você tem o seguinte:
A tarefa é fazer com que a função third
sempre atrase a execução até que a ação assíncrona na função second
tenha sido concluída. É aqui que entram os callbacks. Em vez de executar first
, second
e third
no nível superior de execução, você passará a função third
como argumento para second
. A função second
executará o callback após a conclusão da ação assíncrona.
Aqui estão as três funções com um callback aplicado:
Agora, execute first
e second
, e então passe third
como argumento para second
:
Após executar este bloco de código, você receberá a seguinte saída:
Output1
2
3
Primeiro, 1
será impresso e após o temporizador completar (neste caso, zero segundos, mas você pode alterá-lo para qualquer valor), será impresso 2
e então 3
. Ao passar uma função como callback, você atrasou com sucesso a execução da função até que a API Web assíncrona (setTimeout
) seja concluída.
A principal conclusão aqui é que as funções de retorno de chamada não são assíncronas – setTimeout
é a API Web assíncrona responsável por lidar com tarefas assíncronas. A função de retorno de chamada apenas permite que você seja informado quando uma tarefa assíncrona foi concluída e lida com o sucesso ou falha da tarefa.
Agora que você aprendeu como usar funções de retorno de chamada para lidar com tarefas assíncronas, a próxima seção explica os problemas de aninhar muitas funções de retorno de chamada e criar uma “pirâmide do caos.”
Funções de Retorno de Chamada Aninhadas e a Pirâmide do Caos
As funções de retorno de chamada são uma maneira eficaz de garantir a execução atrasada de uma função até que outra seja concluída e retorne com dados. No entanto, devido à natureza aninhada das funções de retorno de chamada, o código pode ficar bagunçado se você tiver muitas solicitações assíncronas consecutivas que dependem umas das outras. Isso foi uma grande frustração para os desenvolvedores de JavaScript no início, e como resultado, o código contendo funções de retorno de chamada aninhadas é frequentemente chamado de “pirâmide do caos” ou “inferno das funções de retorno de chamada.”
Aqui está uma demonstração de funções de retorno de chamada aninhadas:
Neste código, cada novo setTimeout
é aninhado dentro de uma função de ordem superior, criando uma forma de pirâmide de funções de retorno de chamada mais e mais profundas. Executar este código resultaria no seguinte:
Output1
2
3
Na prática, com código assíncrono do mundo real, isso pode se tornar muito mais complicado. Provavelmente você precisará lidar com erros no código assíncrono e, em seguida, passar alguns dados de cada resposta para a próxima solicitação. Fazer isso com callbacks tornará seu código difícil de ler e manter.
Aqui está um exemplo executável de um “pirâmide do caos” mais realista com o qual você pode brincar:
Neste código, você deve fazer com que cada função considere uma possível response
e um possível error
, tornando a função callbackHell
visualmente confusa.
Executar este código lhe dará o seguinte:
Output
First 9
Second 3
Error: Whoa! Something went wrong.
at asynchronousRequest (<anonymous>:4:21)
at second (<anonymous>:29:7)
at <anonymous>:9:13
Esta forma de lidar com código assíncrono é difícil de acompanhar. Como resultado, o conceito de promessas foi introduzido no ES6. Isso é o foco da próxima seção.
Promessas
A promise represents the completion of an asynchronous function. It is an object that might return a value in the future. It accomplishes the same basic goal as a callback function, but with many additional features and a more readable syntax. As a JavaScript developer, you will likely spend more time consuming promises than creating them, as it is usually asynchronous Web APIs that return a promise for the developer to consume. This tutorial will show you how to do both.
Criando uma Promessa
Você pode inicializar uma promessa com a sintaxe new Promise
, e você deve inicializá-la com uma função. A função passada para uma promessa tem parâmetros resolve
e reject
. As funções resolve
e reject
lidam com o sucesso e o fracasso de uma operação, respectivamente.
Escreva a seguinte linha para declarar uma promessa:
Se você inspecionar a promessa inicializada neste estado com o console do seu navegador da web, você descobrirá que ela tem um status de pending
e um valor undefined
:
Output__proto__: Promise
[[PromiseStatus]]: "pending"
[[PromiseValue]]: undefined
Até agora, nada foi configurado para a promessa, então ela vai permanecer em estado pending
para sempre. A primeira coisa que você pode fazer para testar uma promessa é cumprir a promessa, resolvendo-a com um valor:
Agora, ao inspecionar a promessa, você descobrirá que ela tem um status de fulfilled
e um value
definido com o valor que você passou para resolve
:
Output__proto__: Promise
[[PromiseStatus]]: "fulfilled"
[[PromiseValue]]: "We did it!"
Conforme declarado no início desta seção, uma promessa é um objeto que pode retornar um valor. Após ser cumprida com sucesso, o value
passa de undefined
para ser populado com dados.
A promise can have three possible states: pending, fulfilled, and rejected.
- Pendente – Estado inicial antes de ser resolvida ou rejeitada
- Realizado – Operação bem-sucedida, promessa foi resolvida
- Rejeitado – Operação falhou, promessa foi rejeitada
Após ser realizada ou rejeitada, uma promessa é resolvida.
Agora que você tem uma ideia de como as promessas são criadas, vamos ver como um desenvolvedor pode consumir essas promessas.
Consumindo uma Promessa
A promessa na última seção foi realizada com um valor, mas você também quer ser capaz de acessar o valor. As promessas têm um método chamado then
que será executado após uma promessa atingir resolve
no código. then
retornará o valor da promessa como um parâmetro.
Assim é como você retornaria e registraria o valor
da promessa de exemplo:
A promessa que você criou tinha um [[PromiseValue]]
de Conseguimos!
. Este valor é o que será passado para a função anônima como resposta
:
OutputWe did it!
Até agora, o exemplo que você criou não envolveu uma API da Web assíncrona – apenas explicou como criar, resolver e consumir uma promessa JavaScript nativa. Usando setTimeout
, você pode testar uma solicitação assíncrona.
O código a seguir simula dados retornados de uma solicitação assíncrona como uma promessa:
Usar a sintaxe then
garante que a response
será registrada apenas quando a operação setTimeout
for concluída após 2000
milissegundos. Tudo isso é feito sem aninhar callbacks.
Agora, após dois segundos, a promessa será resolvida e o valor será registrado em then
:
OutputResolving an asynchronous request!
As promessas também podem ser encadeadas para passar dados para mais de uma operação assíncrona. Se um valor for retornado em then
, outro then
pode ser adicionado que será cumprido com o valor retornado do then
anterior:
A resposta cumprida no segundo then
irá registrar o valor retornado:
OutputResolving an asynchronous request! And chaining!
Já que then
pode ser encadeado, permite que o consumo de promessas pareça mais síncrono do que callbacks, pois não precisam ser aninhados. Isso permitirá um código mais legível que pode ser mantido e verificado com mais facilidade.
Manipulação de Erros
Até agora, você apenas lidou com uma promessa com um resolve
bem-sucedido, o que coloca a promessa em um estado fulfilled
. Mas frequentemente, com uma solicitação assíncrona, você também precisa lidar com um erro – se a API estiver fora do ar, ou se uma solicitação malformada ou não autorizada for enviada. Uma promessa deve ser capaz de lidar com ambos os casos. Nesta seção, você criará uma função para testar tanto o caso de sucesso quanto o de erro de criação e consumo de uma promessa.
Esta função getUsers
passará uma sinalização para uma promessa e retornará a promessa:
Configure o código de modo que se onSuccess
for true
, o timeout será resolvido com alguns dados. Se false
, a função será rejeitada com um erro:
Para o resultado bem-sucedido, você retorna objetos JavaScript que representam dados de usuário de exemplo.
Para lidar com o erro, você usará o método de instância catch
. Isso lhe dará um retorno de chamada de falha com o error
como parâmetro.
Execute o comando getUser
com onSuccess
definido como false
, usando o método then
para o caso de sucesso e o método catch
para o erro:
Desde que o erro foi acionado, o then
será ignorado e o catch
lidará com o erro:
OutputFailed to fetch data!
Se você trocar a flag e resolve
ao invés, o catch
será ignorado, e os dados serão retornados:
Isto resultará nos dados do usuário:
Output(3) [{…}, {…}, {…}]
0: {id: 1, name: "Jerry"}
1: {id: 2, name: "Elaine"}
3: {id: 3, name: "George"}
Para referência, aqui está uma tabela com os métodos de manipulação em objetos Promise
:
Method | Description |
---|---|
then() |
Handles a resolve . Returns a promise, and calls onFulfilled function asynchronously |
catch() |
Handles a reject . Returns a promise, and calls onRejected function asynchronously |
finally() |
Called when a promise is settled. Returns a promise, and calls onFinally function asynchronously |
As Promises podem ser confusas, tanto para desenvolvedores novos quanto para programadores experientes que nunca trabalharam em um ambiente assíncrono antes. No entanto, como mencionado, é muito mais comum consumir promises do que criar. Normalmente, uma API Web do navegador ou uma biblioteca de terceiros fornecerá a promise, e você só precisa consumi-la.
Na seção final sobre promises, este tutorial citará um caso de uso comum de uma API Web que retorna promises: a Fetch API.
Usando a Fetch API com Promises
Uma das APIs da Web mais úteis e frequentemente utilizadas que retorna uma promessa é a Fetch API, que permite fazer uma solicitação de recurso assíncrona pela rede. fetch
é um processo de duas partes e, portanto, requer encadeamento de then
. Este exemplo demonstra o uso da API do GitHub para buscar os dados de um usuário, enquanto também lida com possíveis erros:
A solicitação fetch
é enviada para a URL https://api.github.com/users/octocat
, que aguarda assincronamente uma resposta. O primeiro then
passa a resposta para uma função anônima que formata a resposta como dados JSON, em seguida, passa o JSON para um segundo then
que registra os dados no console. A instrução catch
registra qualquer erro no console.
A execução deste código resultará no seguinte:
Outputlogin: "octocat",
id: 583231,
avatar_url: "https://avatars3.githubusercontent.com/u/583231?v=4"
blog: "https://github.blog"
company: "@github"
followers: 3203
...
Estes são os dados solicitados de https://api.github.com/users/octocat
, renderizados no formato JSON.
Esta seção do tutorial mostrou que as promessas incorporam muitas melhorias para lidar com código assíncrono. No entanto, enquanto usar then
para lidar com ações assíncronas é mais fácil de seguir do que a pirâmide de callbacks, alguns desenvolvedores ainda preferem um formato síncrono para escrever código assíncrono. Para atender a essa necessidade, ECMAScript 2016 (ES7) introduziu funções async
e a palavra-chave await
para facilitar o trabalho com promessas.
Funções Assíncronas com async/await
Uma função async
permite que você lide com código assíncrono de uma maneira que parece síncrona. As funções async
ainda usam promises por baixo dos panos, mas têm uma sintaxe mais tradicional do JavaScript. Nesta seção, você experimentará exemplos dessa sintaxe.
Você pode criar uma função async
adicionando a palavra-chave async
antes de uma função:
Embora esta função ainda não esteja lidando com nada assíncrono, ela se comporta de maneira diferente de uma função tradicional. Se você executar a função, verá que ela retorna uma promise com um [[PromiseStatus]]
e [[PromiseValue]]
em vez de um valor de retorno.
Tente isso registrando uma chamada à função getUser
:
Isso resultará no seguinte:
Output__proto__: Promise
[[PromiseStatus]]: "fulfilled"
[[PromiseValue]]: Object
Isso significa que você pode lidar com uma função async
com then
da mesma forma que poderia lidar com uma promise. Experimente isso com o seguinte código:
Essa chamada para getUser
passa o valor de retorno para uma função anônima que registra o valor no console.
Você receberá o seguinte quando executar este programa:
Output{}
Uma função async
pode lidar com uma promessa chamada dentro dela usando o operador await
. O await
pode ser usado dentro de uma função async
e esperará até que uma promessa seja resolvida antes de executar o código designado.
Com esse conhecimento, você pode reescrever a requisição Fetch da última seção usando async
/await
da seguinte forma:
Os operadores await
aqui garantem que os dados
não sejam registrados antes que a requisição os preencha com dados.
Agora os dados finais podem ser tratados dentro da função getUser
, sem a necessidade de usar then
. Este é o resultado do registro de dados
:
Outputlogin: "octocat",
id: 583231,
avatar_url: "https://avatars3.githubusercontent.com/u/583231?v=4"
blog: "https://github.blog"
company: "@github"
followers: 3203
...
Observação: Em muitos ambientes, async
é necessário para usar await
– no entanto, algumas novas versões de navegadores e Node permitem o uso de await
no nível superior, o que permite ignorar a criação de uma função assíncrona para envolver o await
.
Finalmente, como você está lidando com a promessa cumprida dentro da função assíncrona, você também pode lidar com o erro dentro da função. Em vez de usar o método catch
com then
, você usará o padrão try
/catch
para lidar com a exceção.
Adicione o seguinte código destacado:
O programa agora irá pular para o bloco catch
se receber um erro e registrar esse erro no console.
O código assíncrono moderno em JavaScript é mais frequentemente tratado com a sintaxe async
/await
, mas é importante ter um conhecimento prático de como promessas funcionam, especialmente porque as promessas são capazes de recursos adicionais que não podem ser tratados com async
/await
, como combinar promessas com Promise.all()
.
Nota: async
/await
pode ser reproduzido usando geradores combinados com promessas para adicionar mais flexibilidade ao seu código. Para saber mais, confira nosso tutorial Entendendo Generators em JavaScript.
Conclusão
Porque as APIs da Web frequentemente fornecem dados de forma assíncrona, aprender a lidar com o resultado de ações assíncronas é uma parte essencial de ser um desenvolvedor JavaScript. Neste artigo, você aprendeu como o ambiente hospedeiro usa o loop de eventos para lidar com a ordem de execução do código com a pilha e a fila. Você também experimentou exemplos de três maneiras de lidar com o sucesso ou falha de um evento assíncrono, com callbacks, promessas e a sintaxe async
/await
. Por fim, você utilizou a Fetch Web API para lidar com ações assíncronas.
Para mais informações sobre como o navegador lida com eventos paralelos, leia Modelo de Concorrência e o Loop de Eventos na Mozilla Developer Network. Se você deseja aprender mais sobre JavaScript, retorne à nossa série Como Programar em JavaScript.