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, tornou-se cada vez mais necessário realizar operações intensivas como fazer solicitações de rede externa para recuperar dados da API. 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 monotarefa com um modelo de execução síncrono que processa uma operação após a outra, só pode processar uma declaração de cada 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 forma 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 comportamentos de bloqueio, o ambiente do navegador possui muitas APIs da Web que o JavaScript pode acessar de forma assíncrona, ou seja, 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 as APIs da Web assíncronas e lidar com a resposta ou erro dessas operações. Neste artigo, você aprenderá sobre o ciclo de eventos, a forma original de lidar com o comportamento assíncrono por meio de callbacks, a adição de promessas da especificação 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 válidos no ambiente do Node.js, no entanto, o Node.js usa suas próprias APIs em C++ em oposição às APIs da Web do navegador.
O Loop de Eventos
Esta seção explicará como o JavaScript lida com código assíncrono usando o loop de eventos. Primeiro, será feita uma demonstração do loop de eventos em funcionamento, e então serão explicados os dois elementos do loop de eventos: a pilha e a fila.
O código JavaScript que não utiliza nenhuma API da Web assíncrona executará de forma síncrona, um de cada vez, sequencialmente. Isso é demonstrado pelo código de exemplo a seguir, que chama três funções que imprimem um número no console:
Neste código, você define três funções que imprimem números com console.log()
.
Em seguida, escreva chamadas para as funções:
A saída será baseada na ordem em que as funções foram chamadas — primeira()
, segunda()
e depois terceira()
:
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 é o setTimeout
, que define um temporizador e realiza uma ação após um período especificado de tempo. setTimeout
precisa ser assíncrono, caso contrário, todo o navegador permaneceria congelado durante a espera, o que resultaria em uma experiência ruim para o usuário.
Adicione setTimeout
à função segunda
para simular uma solicitação assíncrona:
setTimeout
recebe dois argumentos: a função que será executada de forma assíncrona e o tempo que irá esperar antes de chamar essa função. Neste código, você envolveu console.log
em uma função anônima e a passou para setTimeout
, então 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 resultaria nos números sendo impressos em ordem sequencial. Mas como é assíncrono, a função com o timeout será impressa por último:
Output1
3
2
Independentemente de definir o timeout 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 acontece porque o ambiente host do 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 por vez, ele precisa que o loop de eventos seja informado de quando executar qual instrução específica. O loop de eventos trata 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:
- Adicione
first()
à pilha, executefirst()
que registra1
no console, removafirst()
da pilha. - Adicione
second()
à pilha, executesecond()
que registra2
no console, removasecond()
da pilha. - Adicione
third()
à pilha, executethird()
que registra3
no console, removathird()
da pilha.
O segundo exemplo com setTimout
se parece com isso:
- Adicione
first()
à pilha, executefirst()
que registra1
no console, removafirst()
da pilha. - Adicione
second()
à pilha, executesecond()
.- Adicione
setTimeout()
à pilha, execute osetTimeout()
Web API que inicia um temporizador e adiciona a função anônima à fila, removasetTimeout()
da pilha.
- Adicione
- 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 da Web assíncrona, introduz o conceito da fila, que este tutorial abordará em seguida.
Fila
A fila, também chamada de 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 pendentes, 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 do 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 ele 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 esteja sendo executada no momento, o que poderia ter efeitos indesejados e imprevisíveis.
Nota: Também existe outra fila chamada fila de tarefas ou fila de microtarefas que lida com promessas. Microtarefas como promessas são tratadas com prioridade mais alta do que tarefas de macro 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 em 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 é executada após tudo no contexto de execução principal de nível superior. Mas se você quiser garantir que uma das funções, como a função third
, seja executada após o tempo de espera, então você teria que usar métodos de codificação assíncrona. O tempo de espera aqui pode representar uma chamada de API assíncrona que contém dados. Você quer 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; 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 essa 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:
Neste código, você define uma função fn
, define uma função higherOrderFunction
que recebe uma função callback
como argumento e passa fn
como uma função de retorno de chamada para higherOrderFunction
.
Executar este código resultará no seguinte:
OutputJust a function
Vamos voltar para as funções first
, second
e third
com setTimeout
. Até agora, é isso que você tem:
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 os callbacks entram. 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
, depois 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) ele imprimirá 2
e então 3
. Ao passar uma função como callback, você conseguiu atrasar com sucesso a execução da função até que a API Web assíncrona (setTimeout
) seja concluída.
O ponto principal aqui é que as funções de retorno de chamada não são assíncronas – setTimeout
é a API da 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 for concluída e lida com o sucesso ou falha da tarefa.
Agora que você aprendeu como usar callbacks para lidar com tarefas assíncronas, a próxima seção explica os problemas de aninhar muitos callbacks e criar uma “pirâmide de dor”.
Callbacks Aninhados e a Pirâmide da Dor
Funções de retorno de chamada são uma maneira eficaz de garantir a execução retardada de uma função até que outra seja concluída e retorne com dados. No entanto, devido à natureza aninhada dos callbacks, o código pode acabar ficando 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 JavaScript no início, e como resultado, o código contendo callbacks aninhados é frequentemente chamado de “pirâmide da dor” ou “inferno de callbacks”.
Aqui está uma demonstração de callbacks aninhados:
Neste código, cada novo setTimeout
está aninhado dentro de uma função de ordem superior, criando uma forma de pirâmide de callbacks cada vez mais profunda. 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. Você provavelmente 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 resposta
e um possível erro
, tornando a função callbackHell
visualmente confusa.
Executando este código, você obterá 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
Essa forma de lidar com código assíncrono é difícil de acompanhar. Como resultado, o conceito de promessas foi introduzido no ES6. Esse é 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 que é 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 pendente
e um valor indefinido
:
Output__proto__: Promise
[[PromiseStatus]]: "pending"
[[PromiseValue]]: undefined
Até agora, nada foi configurado para a promessa, então ela vai ficar lá em um estado pendente
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ê verá que ela tem um status de cumprida
e um valor
definido como o valor que você passou para resolve
:
Output__proto__: Promise
[[PromiseStatus]]: "fulfilled"
[[PromiseValue]]: "We did it!"
Conforme afirmado no início desta seção, uma promessa é um objeto que pode retornar um valor. Após ser cumprida com sucesso, o valor
passa de indefinido
para ser preenchido 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 fracassada, 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 deseja ser capaz de acessar esse valor. As promessas têm um método chamado then
que será executado depois que uma promessa alcançar o resolve
no código. then
irá retornar o valor da promessa como parâmetro.
É assim que você retornaria e registraria o valor
da promessa de exemplo:
A promessa que você criou tinha um [[PromiseValue]]
de Nós conseguimos!
. Esse 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 resposta
será registrada somente 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á concluído com o valor de retorno do then
anterior:
A resposta cumprida no segundo then
registrará o valor de retorno:
OutputResolving an asynchronous request! And chaining!
Como o 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.
Tratamento de Erros
Até agora, você apenas tratou uma promessa com um resolve
bem-sucedido, 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 caso de erro de criação e consumo de uma promessa.
Esta função getUsers
passará um sinalizador para uma promessa e retornará a promessa:
Configure o código para que, se onSuccess
for true
, o tempo limite seja concluído 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 amostra.
Para lidar com o erro, você usará o método de instância catch
. Isso fornecerá 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ê alternar a sinalização e resolve
em vez disso, 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 novos desenvolvedores quanto para programadores experientes que nunca trabalharam em um ambiente assíncrono antes. No entanto, como mencionado, é muito mais comum consumir promises do que criá-las. Normalmente, uma API da Web do navegador ou uma biblioteca de terceiros fornecerá a promise, e você só precisa consumi-la.
Na seção final de promessas, este tutorial citará um caso de uso comum de uma API da Web que retorna promessas: a Fetch API.
Usando a Fetch API com Promessas
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íncrono pela rede. fetch
é um processo de duas partes e, portanto, requer encadeamento de then
. Este exemplo demonstra acessar a API do GitHub para buscar os dados de um usuário, enquanto também trata qualquer erro potencial:
A solicitação fetch
é enviada para o URL https://api.github.com/users/octocat
, que espera assincronamente por 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. O comando catch
registra qualquer erro no console.
A execução deste código produzirá o 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 em formato JSON.
Esta seção do tutorial mostrou que 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 assíncrona
permite que você lide com código assíncrono de uma maneira que parece síncrona. As funções async
ainda utilizam promessas por baixo dos panos, mas possuem uma sintaxe mais tradicional em JavaScript. Nesta seção, você irá 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, perceberá que ela retorna uma promessa com um [[PromiseStatus]]
e [[PromiseValue]]
em vez de um valor de retorno.
Tente isso registrando uma chamada à função getUser
:
Isto dará o seguinte:
Output__proto__: Promise
[[PromiseStatus]]: "fulfilled"
[[PromiseValue]]: Object
Isso significa que você pode lidar com uma função async
com then
da mesma maneira que poderia lidar com uma promessa. Experimente isso com o seguinte código:
Esta 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 manipular uma promise chamada dentro dela usando o operador await
. O await
pode ser usado dentro de uma função async
e aguardará até que uma promise seja resolvida antes de executar o código designado.
Com esse conhecimento, você pode reescrever a solicitação Fetch da última seção usando async
/await
da seguinte maneira:
Os operadores await
aqui garantem que os data
não sejam registrados antes que a solicitação os tenha preenchido com dados.
Agora o data
final pode ser tratado dentro da função getUser
, sem necessidade de usar then
. Este é o resultado do registro do data
:
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 o Node permitem o uso de await
no nível superior, o que permite evitar a criação de uma função async para envolver o await
.
Finalmente, como você está lidando com a promise 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 de async
/await
, mas é importante ter um conhecimento prático de como as 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 Geradores 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 utiliza o loop de eventos para lidar com a ordem de execução de código com a pilha e 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 sintaxe async
/await
. Por fim, você utilizou a API Fetch da Web para lidar com ações assíncronas.
Para obter mais informações sobre como o navegador lida com eventos paralelos, leia Modelo de Concorrência e o Loop de Eventos na Rede de Desenvolvedores Mozilla. Se você gostaria de aprender mais sobre JavaScript, retorne à nossa série Como Programar em JavaScript.