Seja Bem-Vindo. Este site tem recursos de leitura de texto, basta marcar o texto e clicar no ícone do alto-falante   Click to listen highlighted text! Seja Bem-Vindo. Este site tem recursos de leitura de texto, basta marcar o texto e clicar no ícone do alto-falante

Linguagem e Técnica de Programação II – Projeto

JavaScript Assíncrono

Aula 1 — Por que precisamos de programação assíncrona?

Nas aulas anteriores, aprendemos a utilizar JavaScript para tornar páginas Web interativas. Trabalhamos com funções, estruturas de controle, arrays, manipulação do DOM e eventos.

Esses conhecimentos permitem construir aplicações que respondem às ações do usuário e modificam dinamicamente a página.

Entretanto, existe um problema que ainda não enfrentamos:

Como obter informações que não estão disponíveis na própria página ou no computador do usuário?

Imagine uma aplicação Web que precise apresentar:

  • A previsão do tempo de uma cidade;
  • A cotação de uma moeda;
  • Os dados de um produto;
  • Informações sobre um filme;
  • Notícias;
  • Os dados de um Pokémon.

Essas informações podem estar armazenadas em outro computador, em um servidor localizado em algum lugar da Internet.

Nesse momento, nosso programa deixa de trabalhar apenas com informações que já possui e passa a precisar se comunicar com outros sistemas.

É nesse contexto que surge a programação assíncrona.

Nesta aula, vamos compreender o problema que torna a programação assíncrona necessária. Ainda não estudaremos Promise, async, await ou fetch() em profundidade. Primeiro precisamos compreender o problema que essas tecnologias ajudam a resolver.

Objetivos

Ao final desta aula, você deverá ser capaz de:

  • Diferenciar uma execução sequencial de uma operação assíncrona;
  • Explicar por que uma operação externa pode levar tempo para ser concluída;
  • Compreender, em nível introdutório, a relação entre cliente e servidor;
  • Identificar o problema do tempo de resposta em uma aplicação Web;
  • Compreender por que JavaScript precisa trabalhar com operações assíncronas;
  • Preparar-se para o estudo de callbacks e Promises.

Projeto da disciplina

Durante as próximas aulas, esses conceitos serão aplicados na construção de uma Pokédex Assíncrona, uma aplicação Web capaz de consultar dados de Pokémon em uma API e apresentá-los dinamicamente na página.

O projeto será desenvolvido progressivamente, acompanhando a evolução dos conceitos estudados.


O PROBLEMA DOS DADOS EXTERNOS

1. Quando os dados não estão na página

Até agora, grande parte das aplicações que construímos utilizava dados que já estavam disponíveis no próprio programa.

Por exemplo:

const nome = 'Pikachu';

console.log(nome);

O programa já conhece a informação.

Também podemos trabalhar com uma estrutura de dados previamente criada:

const pokemon = {
    nome: 'Pikachu',
    tipo: 'Elétrico'
};

console.log(pokemon.nome);

Nesse caso, os dados estão disponíveis imediatamente para o JavaScript.

Agora imagine uma situação diferente.

O usuário deseja consultar os dados de um Pokémon, mas nossa aplicação não possui essas informações armazenadas.

Onde estão os dados?

Eles podem estar em um servidor.

Podemos representar a situação assim:

┌─────────────────────────┐
│       NAVEGADOR         │
│                         │
│      Aplicação Web      │
│       JavaScript        │
└────────────┬────────────┘
             │
             │ Solicitação
             ▼
┌─────────────────────────┐
│        SERVIDOR         │
│                         │
│    Dados dos Pokémon    │
└─────────────────────────┘

O navegador precisa solicitar os dados ao servidor.

O servidor, por sua vez, precisa processar essa solicitação e devolver uma resposta.

Portanto, surge uma nova situação:

Aplicação
    ↓
Solicitação
    ↓
Servidor
    ↓
Processamento
    ↓
Resposta
    ↓
Aplicação

Esse processo não acontece necessariamente de forma instantânea.

A comunicação pela rede pode levar algum tempo.

Além disso, a operação pode apresentar problemas.

O servidor pode estar indisponível.

A conexão pode falhar.

O recurso solicitado pode não existir.

A resposta pode demorar.

Portanto, o JavaScript precisa saber trabalhar com uma situação fundamental:

O resultado de uma operação pode não estar disponível no momento em que a operação é iniciada.

É nesse ponto que começa o estudo da programação assíncrona.


EXECUÇÃO SEQUENCIAL

2. Como o JavaScript executa instruções?

Considere o código:

console.log('Início');

console.log('Processando');

console.log('Fim');

A saída será:

Início
Processando
Fim

As instruções são executadas em sequência.

Podemos representar:

Início
   ↓
Processando
   ↓
Fim

O programa chega a uma instrução, executa essa instrução e prossegue para a próxima.

Esse comportamento é simples de observar.

Agora imagine que uma das operações precise de algum tempo para ser concluída.

Por exemplo:

Início
   ↓
Consultar servidor
   ↓
Esperar resposta
   ↓
Processar resposta
   ↓
Fim

Se o programa precisasse simplesmente interromper toda a execução enquanto aguardasse a resposta, uma operação demorada poderia prejudicar a experiência do usuário.

Precisamos, portanto, de uma forma de lidar com operações cujo resultado será obtido posteriormente.


PRIMEIRO EXPERIMENTO ASSÍNCRONO

3. Uma operação que termina depois

Observe o código:

console.log('Início');

setTimeout(() => {
    console.log('Operação concluída');
}, 3000);

console.log('Fim');

Ao executar o programa, a saída será aproximadamente:

Início
Fim
Operação concluída

Observe atentamente a ordem.

O código foi escrito nesta sequência:

1. Início
2. Operação concluída
3. Fim

Entretanto, a saída foi:

1. Início
2. Fim
3. Operação concluída

Por que isso aconteceu?

A instrução:

setTimeout(() => {
    console.log('Operação concluída');
}, 3000);

solicita ao navegador que execute aquela função posteriormente.

O valor 3000 representa aproximadamente três segundos.

Enquanto esse período passa, o JavaScript continua a execução das instruções seguintes.

Por isso:

console.log('Fim');

é executado antes de:

console.log('Operação concluída');

Podemos representar o comportamento assim:

Início
   ↓
Agenda operação
   ↓
Fim
   ↓
[algum tempo depois]
   ↓
Operação concluída

O setTimeout() está sendo utilizado aqui como uma simulação didática de uma operação que demora para produzir seu resultado.

Uma requisição a um servidor é um exemplo de operação real que pode apresentar esse comportamento temporal.


EXPERIMENTO COM setTimeout(..., 0)

4. Zero milissegundos significa imediatamente?

Observe:

console.log('1');
setTimeout(() => {
    console.log('2');
}, 2000);
console.log('3');
setTimeout(() => {
    console.log('4');
}, 0);
console.log('5');

Antes de executar o programa, faça uma previsão.

Qual será a ordem dos números?

Depois execute o código.

O resultado esperado é:

1
3
5
4
2

Observe especialmente:

setTimeout(() => {
    console.log('4');
}, 0);

O tempo especificado foi:

0 milissegundos

Mesmo assim, 4 não aparece antes de 3 e 5.

Isso ocorre porque setTimeout() não significa:

“Execute esta função imediatamente.”

Ele significa, de forma simplificada:

“Agende esta função para execução posterior, respeitando pelo menos o atraso especificado.”

Portanto:

setTimeout(funcao, 0);

não significa execução imediata.

A função será executada posteriormente, quando o ambiente de execução puder processá-la.


O QUE É PROGRAMAÇÃO ASSÍNCRONA?

5. Uma primeira definição

Podemos definir programação assíncrona, em nível introdutório, da seguinte maneira:

Programação assíncrona é uma forma de trabalhar com operações cuja conclusão pode ocorrer posteriormente, permitindo que a execução prossiga enquanto o resultado ainda não está disponível.

Compare:

Execução sequencial

A
↓
B
↓
C
↓
D

Operação assíncrona

A
↓
Inicia operação
↓
C
↓
D
        \
         \
          Resultado posteriormente

O ponto fundamental é:

Assíncrono não significa mais rápido.

Uma operação assíncrona pode continuar levando três segundos, dez segundos ou mais.

A diferença está na forma como o programa organiza sua execução enquanto aguarda o resultado.

Na Web, isso é particularmente importante porque muitas operações dependem de recursos externos:

  • Servidores;
  • Redes;
  • APIs;
  • Bancos de dados;
  • Arquivos;
  • Serviços Web.

CLIENTE E SERVIDOR

6. Como uma aplicação Web obtém dados?

Quando uma aplicação precisa de dados que estão em outro sistema, ocorre uma comunicação entre um cliente e um servidor.

No nosso projeto, o cliente será o navegador.

┌───────────────────────┐
│        CLIENTE        │
│                       │
│      Navegador        │
│      HTML             │
│      CSS              │
│      JavaScript       │
└───────────┬───────────┘
            │
            │ Requisição
            ▼
┌───────────────────────┐
│       SERVIDOR        │
│                       │
│        API            │
│                       │
│       Dados           │
└───────────┬───────────┘
            │
            │ Resposta
            ▼
┌───────────────────────┐
│        CLIENTE        │
│                       │
│  Processa os dados    │
│  Atualiza a interface │
└───────────────────────┘

Podemos resumir o processo:

Cliente
   ↓
Requisição
   ↓
Servidor
   ↓
Processamento
   ↓
Resposta
   ↓
Cliente

No projeto da Pokédex:

Navegador
   ↓
JavaScript
   ↓
PokéAPI
   ↓
Dados do Pokémon
   ↓
JavaScript
   ↓
DOM
   ↓
Card do Pokémon

Portanto, nossa aplicação precisará aprender a fazer algo que ainda não fizemos:

Solicitar dados a um sistema externo e continuar sua execução enquanto aguarda a resposta.


UMA INTRODUÇÃO AO HTTP

7. Como cliente e servidor se comunicam?

Uma das tecnologias utilizadas nessa comunicação é o HTTP — Hypertext Transfer Protocol.

De forma simplificada, podemos imaginar:

CLIENTE
   │
   │ Requisição HTTP
   ▼
SERVIDOR
   │
   │ Resposta HTTP
   ▼
CLIENTE

Para obter informações, utilizaremos principalmente o método HTTP:

GET

O método GET indica, de maneira simplificada:

Solicite a obtenção de um recurso.

Por exemplo:

GET /pokemon/pikachu

Estamos solicitando ao servidor o recurso associado ao Pokémon pikachu.

Uma URL real da PokéAPI será utilizada posteriormente:

https://pokeapi.co/api/v2/pokemon/pikachu

Nesse momento, ainda não precisamos escrever código para fazer essa requisição.

Primeiro precisamos compreender o modelo:

JavaScript
    ↓
Requisição HTTP GET
    ↓
Servidor
    ↓
Resposta
    ↓
JavaScript

O conteúdo da resposta poderá ser transformado em dados que o programa consiga utilizar.


POR QUE O TEMPO É UM PROBLEMA?

8. E se o servidor demorar?

Considere esta situação:

Aplicação
   ↓
"Quero os dados do Pikachu"
   ↓
Servidor
   ↓
Processando...
   ↓
Resposta

Quanto tempo o servidor levará?

Não podemos saber antecipadamente.

Pode ser:

50 milissegundos

ou:

500 milissegundos

ou:

3 segundos

ou:

10 segundos

Também pode ocorrer uma falha.

Por exemplo:

Internet indisponível
Servidor indisponível
URL incorreta
Recurso inexistente
Erro no servidor

Portanto, uma aplicação Web precisa estar preparada para situações em que:

A resposta ainda não chegou.

E também para situações em que:

A resposta nunca chegará corretamente.

Esse é um dos motivos pelos quais a programação assíncrona é tão importante no desenvolvimento Web.


RELACIONANDO O EXPERIMENTO COM UMA API

9. Do setTimeout() para uma API

No experimento anterior:

setTimeout(() => {
    console.log('Operação concluída');
}, 3000);

simulamos uma operação que termina posteriormente.

Em uma aplicação real, podemos ter:

JavaScript
    ↓
Solicitação HTTP
    ↓
Servidor
    ↓
Processamento
    ↓
Resposta

A situação é conceitualmente semelhante:

Operação iniciada
       ↓
Resultado ainda não disponível
       ↓
Outras atividades podem continuar
       ↓
Resultado chega posteriormente
       ↓
Programa processa o resultado

A diferença é que, no caso da API, estamos realmente realizando uma comunicação pela rede.

Na próxima etapa da formação aprenderemos como representar esse resultado futuro utilizando Promises.


ATIVIDADE PRÁTICA

10. Atividade — Observe, preveja e explique

Execute o código:

console.log('A');
setTimeout(() => {
    console.log('B');
}, 3000);
console.log('C');
setTimeout(() => {
    console.log('D');
}, 1000);
console.log('E');

Questão 1

Qual será a ordem das mensagens exibidas no console?

Questão 2

Por que C aparece antes de B?

Questão 3

Por que E aparece antes de B?

Questão 4

Por que D aparece antes de B?

Questão 5

O que aconteceria se o valor 3000 fosse alterado para 5000?

Questão 6

O que aconteceria se o valor 1000 fosse alterado para 5000?

Questão 7

O seguinte código executa a função imediatamente?

setTimeout(() => {
    console.log('Teste');
}, 0);

Explique.

Questão 8

Qual é a relação entre esse experimento e uma aplicação que precisa consultar um servidor?

Questão 9

Explique, com suas próprias palavras, o que significa dizer que uma operação é assíncrona.


DESAFIO DE RACIOCÍNIO

11. Situação-problema

Imagine uma aplicação que precisa consultar um servidor:

Usuário
   ↓
Clica em "Buscar"
   ↓
JavaScript solicita os dados
   ↓
Servidor processa a solicitação
   ↓
Servidor responde
   ↓
JavaScript apresenta os dados

Agora imagine que o servidor demore três segundos para responder.

Responda:

  1. O usuário precisa necessariamente esperar três segundos sem que nada mais aconteça na aplicação?
  2. O que poderia acontecer com a experiência do usuário se toda a aplicação ficasse bloqueada durante a espera?
  3. Como o programa poderia saber que a operação terminou?
  4. Como o programa poderia lidar com uma situação em que a operação falhou?

Essas perguntas serão respondidas progressivamente nas próximas aulas.


RESUMO DA AULA

12. O que aprendemos?

Nesta aula, vimos que:

  • Uma aplicação Web pode precisar de dados que não estão disponíveis localmente.
  • Esses dados podem estar em um servidor.
  • O navegador pode enviar uma requisição ao servidor.
  • O servidor pode precisar de tempo para processar a solicitação.
  • A resposta pode chegar posteriormente.
  • Uma operação pode ser assíncrona.
  • setTimeout() pode ser utilizado para demonstrar didaticamente uma operação que será executada posteriormente.
  • setTimeout(..., 0) não significa execução imediata.
  • A programação assíncrona é fundamental para aplicações Web que dependem de operações externas.
  • HTTP permite a comunicação entre cliente e servidor.
  • O método GET é utilizado para solicitar a obtenção de recursos.
  • A próxima etapa será compreender como o JavaScript representa o resultado futuro de uma operação assíncrona.

O modelo conceitual desta aula pode ser resumido assim:

APLICAÇÃO WEB
      ↓
DADO EXTERNO
      ↓
SERVIDOR
      ↓
REQUISIÇÃO
      ↓
TEMPO DE ESPERA
      ↓
RESPOSTA POSTERIOR
      ↓
PROGRAMAÇÃO ASSÍNCRONA

Na próxima aula, estudaremos uma primeira maneira de representar a conclusão de uma operação assíncrona: os callbacks. Em seguida, veremos por que as Promises oferecem uma solução mais estruturada.


EXERCÍCIO PARA CASA

Conteúdo para WordPress

13. Exercício de fixação

Crie um arquivo script.js contendo o código:

console.log('Início da aplicação');
setTimeout(() => {
    console.log('Resposta recebida do servidor');
}, 4000);
console.log('Aplicação continua executando');
setTimeout(() => {
    console.log('Atualização da interface');
}, 1000);
console.log('Fim da execução inicial');

Tarefas

  1. Execute o programa e registre a ordem das mensagens.
  2. Explique por que a mensagem "Aplicação continua executando" aparece antes de "Resposta recebida do servidor".
  3. Explique por que "Atualização da interface" aparece antes de "Resposta recebida do servidor".
  4. Modifique os tempos dos dois setTimeout() e observe novamente o resultado.
  5. Crie uma segunda versão utilizando três operações assíncronas.
  6. Antes de executar, faça uma previsão da ordem das mensagens.
  7. Compare sua previsão com o resultado obtido.

Desafio

Imagine que o primeiro setTimeout() represente uma consulta a um servidor.

Escreva, em cinco a dez linhas, uma explicação respondendo:

Por que uma aplicação Web precisa estar preparada para receber uma resposta posteriormente?


JavaScript Assíncrono

 Como saber quando uma operação terminou?

Na aula anterior, estudamos um problema fundamental da programação Web.

Uma aplicação pode iniciar uma operação cujo resultado não estará disponível imediatamente.

Por exemplo:

JavaScript
    ↓
Solicitação
    ↓
Servidor
    ↓
Processamento
    ↓
Resposta

Não sabemos exatamente quando a resposta chegará.

Ela pode chegar rapidamente ou demorar alguns segundos.

Também pode ocorrer uma falha.

Portanto, surge uma nova pergunta:

Como o programa pode saber que uma operação assíncrona terminou?

E, depois:

Como o programa pode receber o resultado dessa operação?

E ainda:

Como o programa pode saber que a operação falhou?

Na Aula 1 utilizamos setTimeout() para visualizar o problema:

console.log('Início');
setTimeout(() => {
    console.log('Operação concluída');
}, 3000);
console.log('Fim');

O resultado foi:

Início
Fim
Operação concluída

A função passada para setTimeout() foi executada posteriormente.

Essa função é um exemplo de callback.

Nesta aula, vamos estudar primeiro os callbacks e, em seguida, conhecer uma abstração mais estruturada para representar operações assíncronas: a Promise.

Objetivos

Ao final desta aula, você deverá ser capaz de:

  • Explicar o conceito de callback;
  • Utilizar uma função como callback;
  • Identificar callbacks utilizados em operações assíncronas;
  • Compreender o problema de encadear muitas operações por callbacks;
  • Explicar o conceito de Promise;
  • Identificar os estados pending, fulfilled e rejected;
  • Utilizar resolve() e reject();
  • Consumir uma Promise com .then();
  • Tratar uma rejeição com .catch();
  • Compreender a ideia de encadeamento de Promises.

CALLBACK: UMA FUNÇÃO QUE SERÁ EXECUTADA DEPOIS

1. Relembrando as funções

Uma função pode ser armazenada em uma variável:

const mensagem = function() {
    console.log('Olá!');
};

Também podemos utilizar uma arrow function:

const mensagem = () => {
    console.log('Olá!');
};

Uma função também pode ser passada como argumento para outra função.

Observe:

function executar(funcao) {
    funcao();
}
executar(() => {
    console.log('Olá!');
});

Nesse exemplo, a função executar() recebe outra função como argumento.

Essa função recebida é chamada de callback.

Definição

Callback é uma função fornecida a outra função ou mecanismo para que seja executada posteriormente ou em resposta a determinado evento ou operação.

O conceito de callback, portanto, não é exclusivo da programação assíncrona.

Uma callback pode ser executada imediatamente:

function executar(funcao) {
    funcao();
}

Ou posteriormente:

function executarDepois(funcao) {
    setTimeout(funcao, 2000);
}

O importante é compreender que:

Uma callback é uma função que será utilizada por outro código em determinado momento.


CALLBACK COM setTimeout()

2. Utilizando uma callback em uma operação posterior

Considere:

function executarDepois(callback) {
    setTimeout(() => {
        callback();
    }, 2000);
}

Agora podemos utilizar a função:

executarDepois(() => {
    console.log('Operação concluída');
});

O fluxo pode ser representado assim:

executarDepois()
       ↓
Recebe uma callback
       ↓
Agenda uma operação
       ↓
2 segundos
       ↓
Executa a callback
       ↓
"Operação concluída"

A callback permite estabelecer uma regra:

Quando a operação terminar, execute esta função.

Essa ideia é extremamente importante.

Podemos imaginar uma operação qualquer:

Iniciar operação
       ↓
     esperar
       ↓
Operação terminou
       ↓
Executar callback

Em uma aplicação real, a operação poderia ser:

Consultar servidor
       ↓
Receber resposta
       ↓
Executar callback

A callback seria o mecanismo utilizado para continuar o processamento.


CALLBACK TAMBÉM PODE RECEBER DADOS

3. A operação pode produzir um resultado

Até agora, nossa callback não recebeu nenhum dado.

Mas uma operação assíncrona normalmente produz algum resultado.

Podemos fazer:

function buscarDados(callback) {
    setTimeout(() => {
        const dados = {
            nome: 'Pikachu',
            id: 25
        };
        callback(dados);
    }, 2000);
}

Agora podemos utilizar:

buscarDados((dados) => {
    console.log(dados);
});

Depois de aproximadamente dois segundos, teremos:

{
    nome: 'Pikachu',
    id: 25
}

Podemos acessar propriedades específicas:

buscarDados((dados) => {
    console.log(dados.nome);
    console.log(dados.id);
});

Resultado:

Pikachu
25

Temos agora um modelo muito importante:

Operação
   ↓
aguarda
   ↓
produz resultado
   ↓
callback(resultado)

O resultado da operação é entregue à função que foi preparada para recebê-lo.


E SE A OPERAÇÃO FALHAR?

4. Operações também podem apresentar erros

Imagine:

JavaScript
    ↓
Solicitação
    ↓
Servidor
    ↓
ERRO

Uma operação assíncrona pode:

  • Ser concluída com sucesso;
  • Falhar;
  • Retornar um resultado inválido;
  • Não encontrar o recurso solicitado;
  • Encontrar um problema de comunicação.

Nos callbacks, uma forma tradicional de representar essa situação é utilizar duas funções:

function buscarDados(sucesso, erro) {
    // operação assíncrona
}

Por exemplo:

function buscarDados(sucesso, erro) {
    setTimeout(() => {
        const funcionou = true;
        if (funcionou) {
            sucesso({
                nome: 'Pikachu',
                id: 25
            });
        } else {
            erro('Não foi possível obter os dados.');
        }
    }, 2000);
}

Uso:

buscarDados(
    (dados) => {
        console.log('Sucesso:', dados);
    },
    (mensagem) => {
        console.error('Erro:', mensagem);
    }
);

Temos agora dois caminhos:

                Operação
                    │
             ┌──────┴──────┐
             │             │
          sucesso         erro
             │             │
             ▼             ▼
           dados        mensagem

Esse modelo funciona.

Entretanto, à medida que a aplicação cresce, o código pode ficar difícil de organizar.

É nesse contexto que surge uma solução mais estruturada:

Promise.


O QUE É UMA PROMISE?

5. Representando um resultado futuro

Uma Promise representa o resultado futuro de uma operação assíncrona.

Em vez de receber imediatamente o resultado da operação, o programa recebe uma representação de algo que:

ainda está sendo processado e produzirá um resultado posteriormente.

Podemos representar:

Operação assíncrona
        ↓
     Promise
        ↓
 ┌──────┴──────┐
 ↓             ↓
Sucesso       Falha
 ↓             ↓
Resultado      Erro

Uma Promise pode ser criada com:

const promessa = new Promise(
    (resolve, reject) => {
        // operação assíncrona
    }
);

A função fornecida ao construtor recebe dois parâmetros especiais:

resolve
reject

resolve() representa a conclusão bem-sucedida.

reject() representa a falha da operação.

Podemos escrever:

const promessa = new Promise(
    (resolve, reject) => {
        setTimeout(() => {
            resolve('Operação concluída');
        }, 2000);
    }
);

A Promise foi criada, mas ainda não sabemos seu resultado imediatamente.

Ela será resolvida depois de aproximadamente dois segundos.


OS ESTADOS DE UMA PROMISE

6. Uma Promise possui estados

Enquanto uma operação assíncrona está em andamento, sua Promise encontra-se inicialmente em estado:

pending

Podemos traduzir como:

pendente

Quando a operação termina com sucesso:

fulfilled

Ou seja:

cumprida/resolvida com sucesso

Quando a operação termina com falha:

rejected

Ou seja:

rejeitada

O ciclo pode ser representado assim:

              PENDING
                 │
        ┌────────┴────────┐
        │                 │
    resolve()          reject()
        │                 │
        ▼                 ▼
   FULFILLED           REJECTED

Uma Promise começa como:

PENDING

e posteriormente termina em um dos dois estados:

FULFILLED

ou:

REJECTED

Ela não volta para pending depois de ter sido estabelecida como concluída ou rejeitada.


resolve() E reject()

7. Como uma Promise termina?

Considere:

const promessa = new Promise(
    (resolve, reject) => {
        resolve('Tudo certo!');
    }
);

A chamada:

resolve('Tudo certo!');

indica:

A operação terminou com sucesso e seu resultado é "Tudo certo!".

Agora:

const promessa = new Promise(
    (resolve, reject) => {
        reject('Algo deu errado!');
    }
);

A chamada:

reject('Algo deu errado!');

indica:

A operação terminou com falha.

Podemos pensar:

resolve(valor)
       ↓
Promise fulfilled
       ↓
valor disponível


reject(erro)
       ↓
Promise rejected
       ↓
erro disponível

CONSUMINDO UMA PROMISE COM .then()

8. Como receber o resultado?

Criar uma Promise não significa que o resultado será automaticamente exibido.

Precisamos informar o que fazer quando ela for cumprida.

Para isso, utilizamos:

.then()

Exemplo:

const promessa = new Promise(
    (resolve, reject) => {
        setTimeout(() => {
            resolve('Dados carregados!');
        }, 2000);
    }
);

Agora:

promessa.then((resultado) => {
    console.log(resultado);
});

O fluxo será:

Promise criada
      ↓
PENDING
      ↓
2 segundos
      ↓
resolve()
      ↓
FULFILLED
      ↓
.then()
      ↓
console.log()

A mensagem:

Dados carregados!

será exibida quando a Promise for cumprida.

Portanto:

.then() define o que será executado quando a Promise for cumprida.


TRATANDO ERROS COM .catch()

9. E quando a Promise for rejeitada?

Considere:

const promessa = new Promise(
    (resolve, reject) => {
        setTimeout(() => {
            reject('Falha ao carregar os dados.');
        }, 2000);
    }
);

Podemos tratar a rejeição utilizando:

.catch()

Exemplo:

promessa
    .then((resultado) => {
        console.log(resultado);
    })
    .catch((erro) => {
        console.error(erro);
    });

O fluxo pode ser representado assim:

                 Promise
                    │
              ┌─────┴─────┐
              │           │
           sucesso       falha
              │           │
              ▼           ▼
           .then()     .catch()
              │           │
              ▼           ▼
          resultado       erro

Assim:

  • .then() trata o sucesso;
  • .catch() trata a rejeição.

Esse padrão será muito importante quando começarmos a trabalhar com APIs.


UMA PROMISE QUE RETORNA DADOS

10. Simulando um servidor

Vamos criar uma função que simula uma consulta a um servidor:

function consultarServidor() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve({
                nome: 'Pikachu',
                id: 25,
                tipo: 'Elétrico'
            });
        }, 2000);
    });
}

A função não retorna diretamente o objeto:

{
    nome: 'Pikachu',
    id: 25,
    tipo: 'Elétrico'
}

Ela retorna uma:

Promise

Essa Promise representará o resultado futuro da consulta.

Podemos utilizá-la:

consultarServidor()
    .then((dados) => {
        console.log(dados);
    });

Depois:

consultarServidor()
    .then((dados) => {
        console.log(dados.nome);
        console.log(dados.id);
        console.log(dados.tipo);
    });

O modelo é:

consultarServidor()
       ↓
    Promise
       ↓
   aguarda
       ↓
    resolve()
       ↓
     dados
       ↓
   .then()

Agora temos uma estrutura muito mais próxima do que encontraremos posteriormente em uma API.


SIMULANDO SUCESSO E FALHA

11. Uma operação pode funcionar ou falhar

Vamos criar uma versão que pode produzir dois resultados:

function consultarServidor() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const sucesso = true;
            if (sucesso) {
                resolve({
                    nome: 'Pikachu',
                    id: 25
                });
            } else {
                reject(
                    new Error(
                        'Servidor indisponível.'
                    )
                );
            }
        }, 2000);
    });
}

Agora podemos consumir:

consultarServidor()
    .then((dados) => {
        console.log('Dados:', dados);
    })
    .catch((erro) => {
        console.error(
            'Erro:',
            erro.message
        );
    });

Se:

const sucesso = true;

o fluxo será:

PENDING
   ↓
resolve()
   ↓
FULFILLED
   ↓
then()

Se:

const sucesso = false;

o fluxo será:

PENDING
   ↓
reject()
   ↓
REJECTED
   ↓
catch()

ENCADEANDO PROMISES

12. Uma Promise pode produzir outra Promise

Uma característica importante das Promises é a possibilidade de encadear operações.

Por exemplo:

consultarServidor()
    .then((dados) => {
        console.log('Primeira operação:', dados);
        return outraOperacao();
    })
    .then((resultado) => {
        console.log('Segunda operação:', resultado);
    })
    .catch((erro) => {
        console.error('Erro:', erro);
    });

A ideia é:

Operação 1
    ↓
Promise
    ↓
.then()
    ↓
Operação 2
    ↓
Promise
    ↓
.then()
    ↓
Resultado final

Esse mecanismo permite organizar sequências de operações assíncronas.

Entretanto, quando muitas operações são encadeadas dessa maneira, o código pode se tornar mais difícil de ler.

É justamente por isso que, na próxima aula, estudaremos uma forma mais legível de escrever esse tipo de código:

async
await

CALLBACK × PROMISE

13. Comparando os dois modelos

Callback

consultarServidor((dados) => {
    console.log(dados);
});

Promise

consultarServidor()
    .then((dados) => {
        console.log(dados);
    });

A diferença conceitual é importante.

No modelo de callback:

Operação
   ↓
Recebe função
   ↓
Executa função posteriormente

No modelo de Promise:

Operação
   ↓
Retorna Promise
   ↓
Promise representa o resultado futuro
   ↓
.then() trata o sucesso
   ↓
.catch() trata a rejeição

A Promise fornece uma estrutura mais padronizada para representar o estado e o resultado de operações assíncronas.


ATIVIDADE PRÁTICA

14. Exercício — Simulando uma consulta a um servidor

Crie a função:

function consultarServidor() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const sucesso = true;
            if (sucesso) {
                resolve({
                    nome: 'Pikachu',
                    id: 25,
                    tipo: 'Elétrico'
                });
            } else {
                reject(
                    new Error(
                        'Não foi possível consultar o servidor.'
                    )
                );
            }
        }, 2000);
    });
}

Depois execute:

consultarServidor()
    .then((dados) => {
        console.log('Nome:', dados.nome);
        console.log('ID:', dados.id);
        console.log('Tipo:', dados.tipo);
    })
    .catch((erro) => {
        console.error(
            'Erro:',
            erro.message
        );
    });

Tarefas

1. Explique por que consultarServidor() retorna uma Promise.

2. Explique o que significa resolve().

3. Explique o que significa reject().

4. Explique quando .then() é executado.

5. Explique quando .catch() é executado.

6. Altere:

const sucesso = true;

para:

const sucesso = false;

e observe o resultado.

7. Modifique os dados retornados pela Promise para representar outro Pokémon.

8. Acrescente uma propriedade:

altura

e apresente seu valor no console.


DESAFIO DE RACIOCÍNIO

15. Desafio — O que realmente está acontecendo?

Considere:

function buscarPokemon() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve({
                nome: 'Pikachu',
                id: 25
            });
        }, 3000);
    });
}
const resultado = buscarPokemon();
console.log(resultado);

Perguntas

1. A variável resultado contém imediatamente o objeto:

{
    nome: 'Pikachu',
    id: 25
}

ou contém outra coisa?

2. Por que o objeto ainda não está disponível imediatamente?

3. Qual é o papel da Promise nesse processo?

4. Como podemos acessar o objeto quando a operação terminar?

5. Qual método podemos utilizar para tratar uma eventual rejeição?


RESUMO DA AULA

16. O que aprendemos?

Nesta aula, aprendemos que:

  • Uma callback é uma função passada para outro código para ser executada em determinado momento.
  • Uma callback não é necessariamente assíncrona.
  • Callbacks podem receber resultados produzidos por operações.
  • Operações assíncronas também podem falhar.
  • Promises representam o resultado futuro de operações assíncronas.
  • Uma Promise possui inicialmente o estado pending.
  • Uma Promise pode terminar como fulfilled.
  • Uma Promise pode terminar como rejected.
  • resolve() indica conclusão bem-sucedida.
  • reject() indica falha.
  • .then() permite tratar o resultado de uma Promise cumprida.
  • .catch() permite tratar uma rejeição.
  • Promises podem ser encadeadas.
  • Promise fornece uma estrutura mais organizada para representar operações assíncronas.
  • async/await, estudado na próxima aula, trabalha sobre Promises.

O modelo conceitual desta aula é:

OPERAÇÃO ASSÍNCRONA
        ↓
      PROMISE
        ↓
     PENDING
        ↓
   ┌────┴────┐
   ↓         ↓
resolve()  reject()
   ↓         ↓
FULFILLED  REJECTED
   ↓         ↓
.then()   .catch()

A próxima etapa será:

Promise
   ↓
async
   ↓
await
   ↓
try/catch
   ↓
fetch()
   ↓
API
   ↓
JSON

EXERCÍCIO PARA CASA

17. Exercício para casa — Cadastro assíncrono

Crie uma função chamada:

carregarUsuario()

Essa função deverá retornar uma Promise.

Após dois segundos, ela deverá ser resolvida com o seguinte objeto:

{
    nome: 'Ana',
    idade: 25,
    profissao: 'Desenvolvedora Web'
}

Requisitos

1. A função deve retornar uma Promise.

2. Utilize setTimeout() para simular o tempo de resposta do servidor.

3. Utilize resolve() para entregar o objeto.

4. Utilize .then() para receber os dados.

5. Utilize .catch() para tratar uma eventual rejeição.

6. Apresente no console:

Nome:
Idade:
Profissão:

Desafio

Modifique a função para que exista uma possibilidade de falha.

Por exemplo:

const sucesso = Math.random() > 0.3;

Se sucesso for verdadeiro:

resolve(dados);

Caso contrário:

reject(
    new Error('Falha ao consultar o servidor.')
);

Execute várias vezes e observe que a operação pode terminar por dois caminhos diferentes:

SUCESSO → resolve() → then()

FALHA   → reject()  → catch()

Questão final

Escreva uma explicação de cinco a dez linhas respondendo:

Por que uma Promise é mais adequada do que simplesmente retornar o resultado de uma operação que ainda não terminou?


DA PROMISE PARA ASYNC/AWAIT

async/await, fetch() e APIs

Na aula anterior, aprendemos que uma Promise representa o resultado futuro de uma operação.

Utilizamos:

consultarServidor()
    .then((dados) => {
        console.log(dados);
    })
    .catch((erro) => {
        console.error(erro);
    });

Esse código funciona.

Porém, quando temos várias operações assíncronas, o encadeamento de .then() pode deixar o código mais difícil de acompanhar.

JavaScript oferece uma sintaxe mais próxima da leitura sequencial tradicional:

async
await

Essas palavras-chave não eliminam as Promises.

Pelo contrário:

async/await é uma forma de trabalhar com Promises utilizando uma sintaxe mais legível.

A partir desta aula, vamos avançar do servidor simulado para uma comunicação real com uma API.

O fluxo será:

Promise
   ↓
async / await
   ↓
try / catch
   ↓
HTTP
   ↓
API
   ↓
fetch()
   ↓
Response
   ↓
JSON
   ↓
Objeto JavaScript

Objetivos

Ao final desta aula, você deverá ser capaz de:

  • Explicar a finalidade de async;
  • Utilizar await com uma Promise;
  • Explicar a relação entre async/await e Promise;
  • Utilizar try/catch para tratar erros em código assíncrono;
  • Compreender o modelo básico de comunicação cliente-servidor;
  • Identificar uma requisição HTTP GET;
  • Compreender o conceito de API e endpoint;
  • Explicar o que é JSON;
  • Utilizar fetch();
  • Compreender o objeto Response;
  • Verificar response.ok;
  • Utilizar response.json();
  • Fazer uma primeira requisição real à PokéAPI.

O QUE ASYNC SIGNIFICA?

1. Criando uma função assíncrona

Na aula anterior, tínhamos uma função que retornava uma Promise:

function consultarServidor() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve({
                nome: 'Pikachu',
                id: 25
            });
        }, 2000);
    });
}

Podíamos utilizar:

consultarServidor()
    .then((dados) => {
        console.log(dados);
    });

Agora podemos criar uma função utilizando async:

async function iniciar() {
    console.log('Iniciando');
}

A palavra:

async

indica que estamos declarando uma função assíncrona.

Uma característica fundamental é:

Uma função declarada com async sempre retorna uma Promise.

Observe:

async function iniciar() {
    return 'Olá';
}

Embora tenhamos escrito:

return 'Olá';

o resultado da função é uma Promise.

Podemos verificar:

const resultado = iniciar();
console.log(resultado);

O console apresentará uma Promise.

Podemos consumi-la:

iniciar()
    .then((mensagem) => {
        console.log(mensagem);
    });

O QUE AWAIT FAZ?

Conteúdo para WordPress

2. Aguardando uma Promise

Considere novamente:

consultarServidor()
    .then((dados) => {
        console.log(dados);
    });

Podemos escrever:

async function iniciar() {
    const dados =
        await consultarServidor();
    console.log(dados);
}

E depois:

iniciar();

O código ficou mais parecido com uma sequência tradicional:

iniciar()
   ↓
consultarServidor()
   ↓
aguardar resultado
   ↓
dados
   ↓
console.log()

A palavra:

await

indica que a função assíncrona deve aguardar a resolução da Promise antes de continuar sua execução naquele ponto.

Portanto:

const dados =
    await consultarServidor();

significa, conceitualmente:

Execute a operação e, quando a Promise produzir seu resultado, atribua esse resultado à variável dados e continue a execução da função.

Atenção

await não bloqueia o navegador inteiro.

Ele suspende a continuação daquela função async enquanto a Promise está pendente.

A interface do navegador continua podendo responder a outras atividades.


COMPARANDO .THEN() E AWAIT

3. Duas formas de consumir uma Promise

Podemos utilizar .then():

consultarServidor()
    .then((dados) => {
        console.log(dados);
    });

Ou await:

async function iniciar() {
    const dados =
        await consultarServidor();
    console.log(dados);
}

iniciar();

As duas formas trabalham com a mesma ideia:

Operação assíncrona
       ↓
    Promise
       ↓
Resultado

A diferença está principalmente na forma como escrevemos a continuação da operação.

Com .then()

Promise
   ↓
.then()
   ↓
resultado

Com await

Promise
   ↓
await
   ↓
resultado

Uma função async pode utilizar await para escrever uma sequência de operações assíncronas de maneira mais próxima da estrutura tradicional do programa.


TRATANDO ERROS COM TRY/CATCH

4. E se a operação falhar?

Na aula anterior, utilizamos:

.catch((erro) => {
    console.error(erro);
});

Com async/await, podemos utilizar:

try
catch

Exemplo:

async function iniciar() {
    try {
        const dados =
            await consultarServidor();
        console.log(dados);
    } catch (erro) {
        console.error(erro);
    }
}

O fluxo será:

                 Promise
                    │
              ┌─────┴─────┐
              │           │
           sucesso       falha
              │           │
              ▼           ▼
            await       catch
              │           │
              ▼           ▼
           dados         erro

Podemos comparar:

Promise com .then() e .catch()

consultarServidor()
    .then((dados) => {
        console.log(dados);
    })
    .catch((erro) => {
        console.error(erro);
    });

async/await com try/catch

async function iniciar() {
    try {
        const dados =
            await consultarServidor();
        console.log(dados);
    } catch (erro) {
        console.error(erro);
    }
}

Os dois modelos trabalham com Promises.


PRIMEIRO CONTATO COM A INTERNET

5. Quando os dados estão fora da aplicação

Até agora, simulamos um servidor com:

setTimeout()

Agora vamos trabalhar com um servidor real.

Imagine uma aplicação que precisa obter informações sobre um Pokémon.

Essas informações não estão necessariamente no computador do usuário.

Elas estão armazenadas em um servidor.

O modelo é:

Navegador
   │
   │ requisição
   ▼
Servidor
   │
   │ resposta
   ▼
Navegador

O navegador é o cliente.

O servidor recebe a solicitação, processa a requisição e devolve uma resposta.

Em uma aplicação Web, essa comunicação normalmente utiliza protocolos da Internet, especialmente o HTTP.


HTTP E O MÉTODO GET

6. Uma requisição HTTP

Uma requisição HTTP pode solicitar determinado recurso.

Por exemplo:

GET /pokemon/pikachu

O método:

GET

indica que queremos obter dados.

Em uma aplicação Web, podemos pensar:

Cliente
   │
   │ GET
   │
   ▼
Servidor
   │
   │ dados
   ▼
Cliente

Outros métodos HTTP existem, como:

GET
POST
PUT
PATCH
DELETE

Nesta aula, nosso foco será o:

GET

porque estamos inicialmente consultando dados.


API E ENDPOINT

7. O que é uma API?

Uma API — Application Programming Interface — fornece uma forma padronizada para que diferentes sistemas possam se comunicar.

No nosso projeto, JavaScript será o cliente que solicitará dados de uma API.

A PokéAPI disponibiliza informações sobre Pokémon.

Um endereço como:

https://pokeapi.co/api/v2/pokemon/pikachu

é um endpoint.

Podemos representá-lo assim:

https://pokeapi.co
       │
       └── /api/v2/pokemon/pikachu
                    │
                    └── recurso solicitado

A aplicação poderá enviar uma requisição para esse endpoint e receber os dados correspondentes.

O fluxo será:

JavaScript
    ↓
Endpoint
    ↓
PokéAPI
    ↓
Resposta HTTP
    ↓
Dados

JSON

8. Como os dados são transportados?

Uma API pode devolver dados em diferentes formatos.

Um formato muito comum em aplicações Web é o JSON — JavaScript Object Notation.

Um exemplo:

{
    "name": "pikachu",
    "id": 25,
    "weight": 60
}

O JSON possui uma estrutura semelhante aos objetos JavaScript.

Podemos ter:

{
    "name": "pikachu",
    "types": [
        {
            "type": {
                "name": "electric"
            }
        }
    ]
}

No JavaScript, depois que os dados forem convertidos para um objeto, poderemos acessar:

dados.name

e:

dados.types[0].type.name

Temos, portanto:

JSON recebido
     ↓
Conversão
     ↓
Objeto JavaScript
     ↓
Propriedades
     ↓
Aplicação

PRIMEIRA REQUISIÇÃO COM FETCH()

9. Utilizando fetch()

Agora estamos preparados para fazer uma requisição real.

O JavaScript fornece a função:

fetch()

Podemos escrever:

const resposta =
    await fetch(
        'https://pokeapi.co/api/v2/pokemon/pikachu'
    );

Observe a sequência:

fetch()
   ↓
Promise
   ↓
await
   ↓
Response

A variável:

resposta

não contém ainda diretamente o Pokémon.

Ela contém um objeto:

Response

Esse objeto representa a resposta HTTP recebida.


VERIFICANDO A RESPOSTA HTTP

10. response.ok

Depois de receber uma resposta, precisamos verificar se a requisição foi considerada bem-sucedida.

Podemos utilizar:

resposta.ok

Exemplo:

const resposta =
    await fetch(
        'https://pokeapi.co/api/v2/pokemon/pikachu'
    );
console.log(resposta.ok);

Em uma resposta HTTP considerada bem-sucedida, teremos:

true

Podemos criar uma validação:

if (!resposta.ok) {
    throw new Error(
        `Erro HTTP: ${resposta.status}`
    );
}

O código completo:

const resposta =
    await fetch(
        'https://pokeapi.co/api/v2/pokemon/pikachu'
    );
if (!resposta.ok) {
    throw new Error(
        `Erro HTTP: ${resposta.status}`
    );
}

Agora temos uma regra:

Se a resposta HTTP não for considerada bem-sucedida, lançamos uma exceção.


O SEGUNDO AWAIT

11. response.json()

Temos agora:

const resposta =
    await fetch(url);

Mas ainda precisamos obter os dados JSON.

Utilizamos:

resposta.json()

Como essa operação também é assíncrona, utilizamos await:

const dados =
    await resposta.json();

Temos então duas etapas:

const resposta =
    await fetch(url);
const dados =
    await resposta.json();

O fluxo completo é:

URL
 ↓
fetch()
 ↓
Promise
 ↓
Response
 ↓
response.json()
 ↓
Promise
 ↓
Objeto JavaScript

Essa sequência é fundamental.

Primeiro await

await fetch(url)

obtém a resposta HTTP.

Segundo await

await resposta.json()

processa o corpo da resposta como JSON.


PRIMEIRO PROGRAMA COMPLETO

12. Consultando o Pikachu

Podemos reunir tudo:

async function buscarPokemon() {
    const resposta =
        await fetch(
            'https://pokeapi.co/api/v2/pokemon/pikachu'
        );
    if (!resposta.ok) {
        throw new Error(
            `Erro HTTP: ${resposta.status}`
        );
    }
    const dados =
        await resposta.json();
    console.log(dados);
}

Agora executamos:

buscarPokemon();

Podemos visualizar informações específicas:

console.log(dados.name);
console.log(dados.id);
console.log(dados.weight);

Porém, existe um problema:

A variável dados foi declarada dentro da função.

Portanto, ela existe naquele escopo.

Podemos então utilizar:

async function buscarPokemon() {
    const resposta =
        await fetch(
            'https://pokeapi.co/api/v2/pokemon/pikachu'
        );
    if (!resposta.ok) {
        throw new Error(
            `Erro HTTP: ${resposta.status}`
        );
    }
    const dados =
        await resposta.json();
    console.log('Nome:', dados.name);
    console.log('ID:', dados.id);
    console.log('Peso:', dados.weight);
}

TRATANDO ERROS CORRETAMENTE

13. Utilizando try/catch

Agora podemos tornar a função mais robusta:

async function buscarPokemon() {
    try {
        const resposta =
            await fetch(
                'https://pokeapi.co/api/v2/pokemon/pikachu'
            );
        if (!resposta.ok) {
            throw new Error(
                `Erro HTTP: ${resposta.status}`
            );
        }
        const dados =
            await resposta.json();
        console.log('Nome:', dados.name);
        console.log('ID:', dados.id);
        console.log('Peso:', dados.weight);
    } catch (erro) {
        console.error(
            'Não foi possível carregar o Pokémon:',
            erro.message
        );
    }
}

Agora temos:

             buscarPokemon()
                    │
                    ▼
                  try
                    │
             ┌──────┴──────┐
             │             │
          sucesso         erro
             │             │
             ▼             ▼
          dados          catch

O catch poderá tratar problemas ocorridos durante o fluxo.


UMA FUNÇÃO REUTILIZÁVEL

14. Transformando a consulta em uma função reutilizável

Em vez de deixar o nome pikachu fixo, podemos receber o Pokémon como parâmetro:

async function buscarPokemon(nome) {
    try {
        const resposta =
            await fetch(
                `https://pokeapi.co/api/v2/pokemon/${nome}`
            );
        if (!resposta.ok) {
            throw new Error(
                `Pokémon não encontrado: ${nome}`
            );
        }
        const dados =
            await resposta.json();
        return dados;
    } catch (erro) {
        console.error(
            'Erro:',
            erro.message
        );
    }
}

Podemos utilizar:

buscarPokemon('pikachu');

Ou:

buscarPokemon('charizard');

Ou:

buscarPokemon('bulbasaur');

Mas ainda precisamos de uma forma de receber o resultado.

Como a função é async, ela retorna uma Promise.

Podemos escrever:

buscarPokemon('pikachu')
    .then((pokemon) => {
        console.log(pokemon);
    });

Ou:

async function iniciar() {
    const pokemon =
        await buscarPokemon('pikachu');
    console.log(pokemon);
}

A PRIMEIRA LISTA DE POKÉMON

15. Consultando vários Pokémon

Agora queremos consultar uma lista.

A PokéAPI permite controlar a quantidade de recursos retornados por meio de parâmetros como limit e offset.

Para nosso primeiro exemplo, utilizaremos:

https://pokeapi.co/api/v2/pokemon?limit=20

Podemos fazer:

async function carregarPokemons() {
    const resposta =
        await fetch(
            'https://pokeapi.co/api/v2/pokemon?limit=20'
        );
    if (!resposta.ok) {
        throw new Error(
            `Erro HTTP: ${resposta.status}`
        );
    }
    const dados =
        await resposta.json();
    console.log(dados);
}

Depois:

carregarPokemons();

A estrutura retornada contém informações sobre os resultados.

Podemos acessar:

dados.results

E percorrer:

dados.results.forEach((pokemon) => {
    console.log(pokemon.name);
});

O fluxo agora é:

API
 ↓
JSON
 ↓
Objeto JavaScript
 ↓
results
 ↓
Array
 ↓
forEach()
 ↓
Nome dos Pokémon

DA API PARA O DOM

16. API → JavaScript → DOM

Agora vamos utilizar um conhecimento que já fazia parte da formação anterior: manipulação do DOM.

Suponha que tenhamos:

<select id="pokemon"></select>

No JavaScript:

const selectPokemon =
    document.querySelector('#pokemon');

Podemos carregar os dados:

async function carregarPokemons() {
    const resposta =
        await fetch(
            'https://pokeapi.co/api/v2/pokemon?limit=20'
        );
    if (!resposta.ok) {
        throw new Error(
            `Erro HTTP: ${resposta.status}`
        );
    }
    const dados =
        await resposta.json();
    dados.results.forEach((pokemon) => {
        const option =
            document.createElement('option');
        option.value =
            pokemon.name;
        option.textContent =
            pokemon.name;
        selectPokemon.appendChild(option);
    });
}

Agora temos uma cadeia completa:

Servidor
   ↓
HTTP
   ↓
fetch()
   ↓
Response
   ↓
JSON
   ↓
Objeto JavaScript
   ↓
Array
   ↓
forEach()
   ↓
DOM
   ↓
<select>

A aplicação deixou de trabalhar apenas com dados estáticos.

Ela está obtendo dados externos e utilizando-os para construir dinamicamente a interface.


Orientação ao professor

Este é o primeiro momento em que o aluno deve perceber a integração entre os conhecimentos anteriores e o novo conteúdo.

Não reensinar:

querySelector()
createElement()
appendChild()

O material anterior já trabalha esses recursos e estabelece o DOM como base para avançar para consumo de dados e programação assíncrona.

Aqui eles entram apenas como ferramentas.


ATIVIDADE PRÁTICA

17. Exercício — Sua primeira API

Crie uma página contendo:

<button id="btnBuscar">
    Buscar Pokémon
</button>
<section id="resultado"></section>

No JavaScript, obtenha os elementos:

const btnBuscar =
    document.querySelector('#btnBuscar');
const resultado =
    document.querySelector('#resultado');

Crie a função:

async function buscarPokemon() {
    // código
}

Ela deverá:

1. Fazer uma requisição para:

https://pokeapi.co/api/v2/pokemon/pikachu

2. Utilizar fetch().

3. Utilizar await.

4. Verificar response.ok.

5. Converter a resposta para JSON.

6. Exibir no console:

Nome
ID
Peso

7. Exibir pelo menos o nome do Pokémon dentro de:

<section id="resultado"></section>

8. Utilizar try/catch.

Desafio

Troque:

pikachu

por:

charizard

Depois:

bulbasaur

Depois:

mewtwo

Observe como a mesma função consegue trabalhar com diferentes recursos.


DESAFIO DE RACIOCÍNIO

18. Desafio — O que existe em cada etapa?

Considere:

const resposta =
    await fetch(
        'https://pokeapi.co/api/v2/pokemon/pikachu'
    );

Questão 1

O que a variável resposta representa?

A) O nome do Pokémon
B) O objeto completo do Pokémon
C) Um objeto Response
D) Uma string JSON

Questão 2

Depois de:

const dados =
    await resposta.json();

o que dados representa?

A) Uma nova requisição HTTP
B) Os dados convertidos para estruturas JavaScript
C) O objeto Response
D) Uma URL

Questão 3

Por que existem dois await?

const resposta =
    await fetch(url);
const dados =
    await resposta.json();

Questão 4

Por que fazemos:

if (!resposta.ok) {
    throw new Error(...);
}

Questão 5

Qual é a diferença conceitual entre:

fetch(url)

e:

resposta.json()

RESUMO DA AULA

19. O que aprendemos?

Nesta aula, avançamos de Promises simuladas para comunicação real com uma API.

Aprendemos que:

  • Uma função async sempre retorna uma Promise.
  • await permite aguardar o resultado de uma Promise dentro de uma função async.
  • await não bloqueia o navegador inteiro.
  • try/catch pode ser utilizado para tratar erros em código assíncrono.
  • HTTP é utilizado para a comunicação entre cliente e servidor na Web.
  • GET é utilizado para solicitar recursos.
  • Uma API fornece uma interface para comunicação entre sistemas.
  • Um endpoint identifica um recurso ou operação disponibilizado pela API.
  • JSON é um formato textual de representação de dados.
  • fetch() realiza uma requisição e retorna uma Promise.
  • fetch() produz um objeto Response.
  • response.ok permite verificar se a resposta HTTP foi considerada bem-sucedida.
  • response.json() processa o corpo da resposta como JSON.
  • response.json() também retorna uma Promise.
  • Os dados convertidos podem ser utilizados como objetos e arrays JavaScript.
  • Esses dados podem posteriormente ser utilizados para modificar o DOM.

O modelo central desta aula é:

                URL
                 ↓
              fetch()
                 ↓
              Promise
                 ↓
              await
                 ↓
             Response
                 ↓
          response.json()
                 ↓
              Promise
                 ↓
              await
                 ↓
       Objeto JavaScript
                 ↓
                DOM

EXERCÍCIO PARA CASA

20. Exercício para casa — Consumindo uma API

Crie uma página que permita pesquisar um Pokémon pelo nome.

Utilize:

<input
    type="text"
    id="pokemon"
    placeholder="Digite o nome do Pokémon"
>
<button id="btnBuscar">
    Buscar
</button>

<section id="resultado"></section>

A aplicação deverá utilizar a PokéAPI.

Requisitos

1. Criar uma função async.

2. Ler o nome digitado pelo usuário.

3. Utilizar fetch().

4. Utilizar await.

5. Verificar response.ok.

6. Utilizar response.json().

7. Utilizar try/catch.

8. Apresentar na página:

Nome
ID
Peso
Tipo

9. Caso o Pokémon não exista, apresentar uma mensagem de erro.

Desafio

Apresente também a imagem oficial do Pokémon.

A informação poderá ser obtida a partir da estrutura retornada pela API:

pokemon.sprites

Procure identificar a estrutura necessária antes de escrever o código.

Questão conceitual

Escreva, com suas próprias palavras, uma resposta para:

Por que precisamos de await tanto em fetch(url) quanto em response.json()?


De JavaScript para aplicações conectadas

Até aqui, construímos uma progressão:

AULA 1
──────────────────────────────
Por que precisamos de assincronismo?
        ↓
Operações demoradas
        ↓
setTimeout()
        ↓
Tempo de resposta
        ↓
Execução assíncrona


AULA 2
──────────────────────────────
Como saber quando terminou?
        ↓
Callback
        ↓
Callback + resultado
        ↓
Callback + erro
        ↓
Promise
        ↓
pending / fulfilled / rejected
        ↓
resolve / reject
        ↓
then / catch


AULA 3
──────────────────────────────
Como escrever Promises de forma mais legível?
        ↓
async
        ↓
await
        ↓
try / catch
        ↓
HTTP
        ↓
API
        ↓
JSON
        ↓
fetch()
        ↓
Response
        ↓
response.json()
        ↓
Objeto JavaScript
        ↓
DOM


AULA 4
──────────────────────────────
Projeto Pokédex
        ↓
Uma requisição
        ↓
Uma resposta
        ↓
Um Pokémon
        ↓
Dois Pokémon
        ↓
Duas requisições
        ↓
Promise.all()
        ↓
Cards
        ↓
Pokédex Assíncrona

A formação passa, portanto, de uma pergunta abstrata:

“O que acontece quando uma operação demora?”

para uma competência prática:

“Como uma aplicação JavaScript solicita dados a uma API, aguarda a resposta, processa os dados e os utiliza para construir sua interface?”

Esse é o ponto em que o estudo de programação assíncrona deixa de ser apenas sintaxe e passa a representar uma competência real de desenvolvimento Web.


DA PRIMEIRA REQUISIÇÃO À POKÉDEX

Promise.all() e construção da Pokédex

Nas aulas anteriores, estudamos progressivamente como uma aplicação JavaScript pode trabalhar com operações assíncronas.

Na Aula 1, compreendemos o problema do tempo de resposta.

Na Aula 2, estudamos callbacks e Promises.

Na Aula 3, utilizamos:

async
await
fetch()

para realizar uma comunicação real com uma API.

Agora vamos utilizar esses conhecimentos para construir uma aplicação completa.

O projeto será uma Pokédex Assíncrona.

A aplicação deverá permitir selecionar dois Pokémon, consultar seus dados na PokéAPI e apresentar os resultados na página.

A sequência será:

Usuário
   ↓
Seleciona dois Pokémon
   ↓
Clica em "Buscar"
   ↓
JavaScript inicia duas consultas
   ↓
Promise.all()
   ↓
Dados dos dois Pokémon
   ↓
Renderização
   ↓
Cards

Objetivos

Ao final desta aula, você deverá ser capaz de:

  • Compreender o problema de realizar várias requisições independentes;
  • Diferenciar execução sequencial de execução concorrente de operações assíncronas;
  • Utilizar Promise.all();
  • Interpretar os resultados produzidos por Promise.all();
  • Utilizar async/await com múltiplas Promises;
  • Utilizar desestruturação para receber múltiplos resultados;
  • Integrar dados de uma API com o DOM;
  • Criar componentes visuais a partir de dados recebidos;
  • Implementar estados básicos de carregamento;
  • Implementar tratamento de erros;
  • Integrar esses conceitos em uma aplicação Web funcional.

UMA REQUISIÇÃO FUNCIONA

Antes de trabalhar com duas requisições, precisamos compreender claramente o que já conseguimos fazer.

Considere:

async function buscarPokemon(nome) {
    const resposta = await fetch(
        `https://pokeapi.co/api/v2/pokemon/${nome}`
    );
    if (!resposta.ok) {
        throw new Error(
            `Erro HTTP: ${resposta.status}`
        );
    }
    const dados = await resposta.json();
    return dados;
}

A função recebe o nome de um Pokémon.

Por exemplo:

const pokemon = await buscarPokemon('pikachu');

Depois que a operação terminar, pokemon conterá os dados retornados pela API.

Podemos acessar propriedades:

console.log(pokemon.name);
console.log(pokemon.id);
console.log(pokemon.weight);

O fluxo é:

nome
 ↓
buscarPokemon()
 ↓
fetch()
 ↓
Response
 ↓
response.json()
 ↓
objeto JavaScript

Portanto, já sabemos consultar um recurso.

Agora surge um novo problema:

E se precisarmos consultar dois recursos independentes?


O PROBLEMA DE DUAS REQUISIÇÕES

Imagine que o usuário tenha escolhido:

Pokémon 1: Pikachu

Pokémon 2: Charizard

Precisamos consultar:

https://pokeapi.co/api/v2/pokemon/pikachu

e:

https://pokeapi.co/api/v2/pokemon/charizard

Uma primeira solução seria:

const pokemon1 =
    await buscarPokemon('pikachu');
const pokemon2 =
    await buscarPokemon('charizard');

Essa solução funciona.

Mas observe a sequência:

Inicia Pikachu
       ↓
Aguarda Pikachu
       ↓
Pikachu termina
       ↓
Inicia Charizard
       ↓
Aguarda Charizard
       ↓
Charizard termina

As duas consultas são independentes.

Para consultar Charizard, não precisamos esperar o resultado de Pikachu.

Então surge uma questão:

Por que iniciar a segunda operação somente depois que a primeira terminou?


PRIMEIRA EXPERIÊNCIA COM DUAS PROMISES

Podemos iniciar as duas operações antes de esperar pelos resultados:

const promessa1 =
    buscarPokemon('pikachu');
const promessa2 =
    buscarPokemon('charizard');

Agora temos:

promessa1 → consulta do Pikachu
promessa2 → consulta do Charizard

Cada variável contém uma Promise.

Podemos então aguardar cada resultado:

const pokemon1 =
    await promessa1;
const pokemon2 =
    await promessa2;

O código já permite iniciar as duas operações antes de aguardar seus resultados.

Entretanto, ainda precisamos de uma maneira organizada de dizer:

“Quero aguardar um conjunto de Promises.”

É exatamente esse problema que Promise.all() resolve.


CONHECENDO PROMISE.ALL()

Promise.all() recebe um conjunto de Promises e retorna uma nova Promise.

Exemplo:

const resultados = await Promise.all([
    buscarPokemon('pikachu'),
    buscarPokemon('charizard')
]);

Quando as duas operações forem concluídas, resultados conterá os dois valores.

Podemos imaginar:

Promise.all()
      │
      ├── buscarPokemon('pikachu')
      │
      └── buscarPokemon('charizard')
               │
               ▼
          dois resultados

O resultado será um array:

[
    dadosDoPikachu,
    dadosDoCharizard
]

A ordem dos resultados corresponde à ordem das Promises fornecidas.

Portanto:

const resultados = await Promise.all([
    buscarPokemon('pikachu'),
    buscarPokemon('charizard')
]);

significa:

resultados[0] → Pikachu
resultados[1] → Charizard

Podemos então escrever:

const pokemon1 = resultados[0];
const pokemon2 = resultados[1];

DESESTRUTURAÇÃO DOS RESULTADOS

Podemos escrever o código anterior de maneira mais direta utilizando desestruturação.

Em vez de:

const resultados = await Promise.all([
    buscarPokemon('pikachu'),
    buscarPokemon('charizard')
]);
const pokemon1 = resultados[0];
const pokemon2 = resultados[1];

podemos escrever:

const [
    pokemon1,
    pokemon2
] = await Promise.all([
    buscarPokemon('pikachu'),
    buscarPokemon('charizard')
]);

Agora temos:

pokemon1 → dados do Pikachu
pokemon2 → dados do Charizard

O código completo fica:

async function buscarDoisPokemons() {
    const [
        pokemon1,
        pokemon2
    ] = await Promise.all([
        buscarPokemon('pikachu'),
        buscarPokemon('charizard')
    ]);
    console.log(pokemon1);
    console.log(pokemon2);
}

Podemos executar:

buscarDoisPokemons();

E SE UMA DAS REQUISIÇÕES FALHAR?

Existe uma característica importante de Promise.all().

Imagine:

const [
    pokemon1,
    pokemon2
] = await Promise.all([
    buscarPokemon('pikachu'),
    buscarPokemon('pokemon-que-nao-existe')
]);

Uma das operações falhará.

Nesse caso, a Promise retornada por Promise.all() será rejeitada.

Por isso, precisamos utilizar:

try {
    // operações
} catch (erro) {
    // tratamento do erro
}

Exemplo:

async function buscarDoisPokemons() {
    try {
        const [
            pokemon1,
            pokemon2
        ] = await Promise.all([
            buscarPokemon('pikachu'),
            buscarPokemon('pokemon-que-nao-existe')
        ]);
        console.log(pokemon1);
        console.log(pokemon2);
    } catch (erro) {
        console.error(erro);
    }
}

O fluxo pode ser representado assim:

             Promise.all()
                  │
        ┌─────────┴─────────┐
        │                   │
    Consulta 1          Consulta 2
        │                   │
     sucesso              erro
        │                   │
        └─────────┬─────────┘
                  ↓
               catch

PREPARANDO A INTERFACE

Agora vamos aplicar o conceito ao projeto.

A interface terá dois campos de seleção:

<select id="pokemon1"></select>
<select id="pokemon2"></select>
<button id="btnBuscar">
    Buscar Pokémons
</button>
<section id="cards"></section>

O JavaScript poderá localizar esses elementos:

const selectPokemon1 =
    document.querySelector('#pokemon1');
const selectPokemon2 =
    document.querySelector('#pokemon2');
const btnBuscar =
    document.querySelector('#btnBuscar');
const cardsContainer =
    document.querySelector('#cards');

O fluxo da aplicação será:

<select> Pokémon 1
       +
<select> Pokémon 2
       ↓
Botão Buscar
       ↓
renderizarPokemons()
       ↓
buscarPokemon()
       ↓
Promise.all()
       ↓
dados dos dois Pokémon
       ↓
criarCard()
       ↓
DOM

O DOM não é o responsável por realizar a consulta.

O DOM fornece a entrada e recebe o resultado.

A comunicação com a API é realizada pelo JavaScript assíncrono.


CARREGANDO OS POKÉMON NOS SELECT

A PokéAPI permite consultar uma lista de Pokémon.

Utilizaremos inicialmente:

https://pokeapi.co/api/v2/pokemon?limit=20

Podemos criar:

async function carregarPokemons() {
    const resposta = await fetch(
        'https://pokeapi.co/api/v2/pokemon?limit=20'
    );
    if (!resposta.ok) {
        throw new Error(
            `Erro HTTP: ${resposta.status}`
        );
    }
    const dados = await resposta.json();
    console.log(dados.results);
}

A propriedade:

dados.results

contém um array.

Cada elemento possui informações como:

{
    name: 'bulbasaur',
    url: 'https://pokeapi.co/api/v2/pokemon/1/'
}

Podemos transformar cada item em uma opção:

const optionsHTML =
    dados.results
        .map(pokemon => `
            <option value="${pokemon.name}">
                ${pokemon.name}
            </option>
        `)
        .join('');

Depois:

selectPokemon1.innerHTML =
    optionsHTML;
selectPokemon2.innerHTML =
    optionsHTML;

Assim, os dois campos receberão a mesma lista de Pokémon.


CRIANDO A FUNÇÃO buscarPokemon()

Agora precisamos de uma função que consulte um Pokémon específico.

async function buscarPokemon(nome) {
    const resposta = await fetch(
        `https://pokeapi.co/api/v2/pokemon/${nome}`
    );
    if (!resposta.ok) {
        throw new Error(
            `Pokémon não encontrado: ${nome}`
        );
    }
    const dados = await resposta.json();
    return dados;
}

Essa função recebe:

nome

e retorna:

dados

Por exemplo:

const pokemon =
    await buscarPokemon('pikachu');

Agora podemos acessar:

pokemon.name
pokemon.id
pokemon.weight
pokemon.types
pokemon.abilities
pokemon.sprites

A função é reutilizável.

Podemos chamar:

await buscarPokemon('pikachu');

ou:

await buscarPokemon('charizard');

ou:

await buscarPokemon('bulbasaur');

AS DUAS CONSULTAS COM PROMISE.ALL()

Agora temos todas as peças necessárias.

Precisamos ler os valores selecionados:

const nome1 =
    selectPokemon1.value;
const nome2 =
    selectPokemon2.value;

Depois consultar os dois Pokémon:

const [
    pokemon1,
    pokemon2
] = await Promise.all([
    buscarPokemon(nome1),
    buscarPokemon(nome2)

]);

O código completo da função será:

async function buscarDoisPokemons() {
    const nome1 =
        selectPokemon1.value;
    const nome2 =
        selectPokemon2.value;
    const [
        pokemon1,
        pokemon2
    ] = await Promise.all([
        buscarPokemon(nome1),
        buscarPokemon(nome2)
    ]);
    console.log(pokemon1);
    console.log(pokemon2);
}

Podemos associar a função ao botão:

btnBuscar.addEventListener(
    'click',
    buscarDoisPokemons
);

Agora o usuário pode selecionar dois Pokémon e solicitar seus dados.


CRIANDO OS CARDS

Agora os dados já estão disponíveis.

Precisamos apresentá-los na página.

Podemos criar uma função:

function criarCard(pokemon) {
    const nome =
        pokemon.name;
    const id =
        pokemon.id;
    const peso =
        pokemon.weight;
    const tipo =
        pokemon.types[0].type.name;
    const habilidade =
        pokemon.abilities[0].ability.name;
    const imagem =
        pokemon.sprites
            .other['official-artwork']
            .front_default;
    cardsContainer.insertAdjacentHTML(
        'beforeend',
        `
        <article class="card">
            <img
                src="${imagem}"
                alt="${nome}"
            >
            <h2>${nome}</h2>
            <p>ID: ${id}</p>
            <p>Tipo: ${tipo}</p>
            <p>Peso: ${peso}</p>
            <p>
                Habilidade: ${habilidade}
            </p>
        </article>
        `
    );
}

Agora podemos utilizar:

criarCard(pokemon1);
criarCard(pokemon2);

O fluxo passa a ser:

API
 ↓
JSON
 ↓
Objeto Pokémon
 ↓
criarCard()
 ↓
HTML
 ↓
DOM

A aplicação deixa de apenas mostrar dados no console.

Agora os dados recebidos pela API passam a formar parte da interface.


INTEGRANDO A RENDERIZAÇÃO

Agora podemos reunir as etapas.

async function renderizarPokemons() {
    try {
        cardsContainer.innerHTML = '';
        const nome1 =
            selectPokemon1.value;
        const nome2 =
            selectPokemon2.value;
        const [
            pokemon1,
            pokemon2
        ] = await Promise.all([
            buscarPokemon(nome1),
            buscarPokemon(nome2)
        ]);
        criarCard(pokemon1);
        criarCard(pokemon2);
    } catch (erro) {
        console.error(erro);
        cardsContainer.innerHTML =
            '<p>Erro ao carregar os dados.</p>';
    }
}

E associamos ao botão:

btnBuscar.addEventListener(
    'click',
    renderizarPokemons
);

A arquitetura agora é:

CLIQUE
  ↓
renderizarPokemons()
  ↓
ler selects
  ↓
buscarPokemon()
  ↓
Promise.all()
  ↓
pokemon1 + pokemon2
  ↓
criarCard()
  ↓
DOM

Essa função reúne os conceitos estudados durante as quatro aulas.


ESTADO DE CARREGAMENTO

Existe ainda um problema de interface.

Quando o usuário clicar em:

Buscar Pokémons

as requisições poderão levar algum tempo.

Durante esse período, a aplicação não deve parecer travada.

Podemos informar:

Carregando Pokémon...

Antes da consulta:

cardsContainer.innerHTML =
    '<p>Carregando Pokémon...</p>';

Depois que os dados chegarem:

criarCard(pokemon1);
criarCard(pokemon2);

Assim, temos três situações principais:

IDLE
Aplicação pronta
       ↓
LOADING
Carregando dados
       ↓
SUCCESS
Dados apresentados

Se ocorrer um erro:

LOADING
   ↓
ERROR
   ↓
Mensagem apresentada

Uma aplicação assíncrona precisa considerar não apenas os dados, mas também o estado da interface durante a operação.


TRATAMENTO DE ERRO PARA O USUÁRIO

Erros fazem parte de aplicações que dependem de serviços externos.

Por exemplo:

  • O servidor pode estar indisponível;
  • A conexão pode falhar;
  • O recurso pode não existir;
  • A API pode retornar um erro HTTP;
  • Um dado inesperado pode provocar um erro durante a renderização.

Por isso, utilizamos:

try {
    // operação
} catch (erro) {
    // tratamento
}

Na aplicação, podemos apresentar:

cardsContainer.innerHTML =
    `
    <p>
        Não foi possível carregar os dados.
        Tente novamente.
    </p>
    `;

O console pode receber informações técnicas:

console.error(erro);

Assim, temos duas finalidades diferentes:

Usuário
   ↓
Mensagem simples e compreensível

Desenvolvedor
   ↓
Informação técnica no console

ATENÇÃO AO USO DE DADOS EXTERNOS

A aplicação recebe informações de uma fonte externa.

Esses dados devem ser tratados como dados, e não automaticamente como HTML confiável.

Por exemplo:

element.textContent = valor;

insere um valor como texto.

Já:

element.innerHTML = valor;

interpreta o conteúdo como HTML.

Em nosso projeto, utilizamos uma API conhecida e controlada para fins didáticos.

Mesmo assim, é importante desenvolver uma regra profissional:

Dados externos não devem ser considerados automaticamente confiáveis para inserção como HTML.

Ao desenvolver aplicações reais, é necessário analisar a origem dos dados e escolher cuidadosamente a técnica de renderização.


CÓDIGO FINAL DA APLICAÇÃO

Depois de construir cada etapa, podemos reunir o código.

'use strict';
const API_URL =
    'https://pokeapi.co/api/v2/pokemon';
const selectPokemon1 =
    document.querySelector('#pokemon1');
const selectPokemon2 =
    document.querySelector('#pokemon2');
const btnBuscar =
    document.querySelector('#btnBuscar');
const cardsContainer =
    document.querySelector('#cards');
async function fetchJSON(url) {
    const response =
        await fetch(url);
    if (!response.ok) {
        throw new Error(
            `Erro HTTP: ${response.status}`
        );
    }
    return await response.json();
}
async function carregarPokemons() {
    const dados =
        await fetchJSON(
            `${API_URL}?limit=20`
        );
    const optionsHTML =
        dados.results
            .map(pokemon => `
                <option value="${pokemon.name}">
                    ${pokemon.name}
                </option>
            `)
            .join('');
    selectPokemon1.innerHTML =
        optionsHTML;
    selectPokemon2.innerHTML =
        optionsHTML;
}
async function buscarPokemon(nome) {
    return await fetchJSON(
        `${API_URL}/${nome}`
    );
}
function criarCard(pokemon) {
    const nome =
        pokemon.name;
    const id =
        pokemon.id;
    const peso =
        pokemon.weight;
    const tipo =
        pokemon.types[0]
            .type.name;
    const habilidade =
        pokemon.abilities[0]
            .ability.name;
    const imagem =
        pokemon.sprites
            .other['official-artwork']
            .front_default;
    cardsContainer.insertAdjacentHTML(
        'beforeend',
        `
        <article class="card">
            <img
                src="${imagem}"
                alt="${nome}"
            >
            <h2>${nome}</h2>
            <p>ID: ${id}</p>
            <p>Tipo: ${tipo}</p>
            <p>Peso: ${peso}</p>
            <p>
                Habilidade: ${habilidade}
            </p>
        </article>
        `
    );
}
async function renderizarPokemons() {
    try {
        cardsContainer.innerHTML =
            '<p>Carregando Pokémon...</p>';
        const nome1 =
            selectPokemon1.value;
        const nome2 =
            selectPokemon2.value;
        const [
            pokemon1,
            pokemon2
        ] = await Promise.all([
            buscarPokemon(nome1),
            buscarPokemon(nome2)
        ]);
        cardsContainer.innerHTML = '';
        criarCard(pokemon1);
        criarCard(pokemon2);
    } catch (erro) {
        console.error(erro);
        cardsContainer.innerHTML =
            `
            <p>
                Não foi possível carregar
                os dados dos Pokémon.
            </p>
            `;
    }
}
btnBuscar.addEventListener(
    'click',
    renderizarPokemons
);
carregarPokemons();

O código final não deve ser entendido como uma sequência de comandos isolados.

Ele representa uma arquitetura:

carregarPokemons()
        ↓
lista da API
        ↓
<select>

Usuário
        ↓
evento click
        ↓
renderizarPokemons()
        ↓
Promise.all()
        ↓
dois objetos Pokémon
        ↓
criarCard()
        ↓
DOM

Essa é a arquitetura básica de uma aplicação Web que consome dados de uma API de maneira assíncrona.


EXERCÍCIOS DE CONSOLIDAÇÃO

Exercícios

Exercício 1 — Uma requisição

Modifique a aplicação para apresentar apenas um Pokémon.

Utilize:

buscarPokemon()

e apresente:

  • Nome;
  • ID;
  • Peso;
  • Tipo;
  • Imagem.

Exercício 2 — Duas requisições sequenciais

Implemente:

const pokemon1 =
    await buscarPokemon('pikachu');

const pokemon2 =
    await buscarPokemon('charizard');

Observe a estrutura da execução.


Exercício 3 — Duas requisições independentes

Modifique o código para iniciar as duas consultas antes de aguardar os resultados.


Exercício 4 — Promise.all()

Utilize:

const [
    pokemon1,
    pokemon2
] = await Promise.all([
    buscarPokemon('pikachu'),
    buscarPokemon('charizard')
]);

Apresente os dois resultados no console.


Exercício 5 — Tratamento de erro

Substitua um dos nomes por:

pokemon-inexistente

Observe o comportamento da aplicação.

Depois implemente uma mensagem amigável para o usuário.


Exercício 6 — Terceiro Pokémon

Modifique a aplicação para trabalhar com três Pokémon.

Utilize:

Promise.all([
    buscarPokemon('pikachu'),
    buscarPokemon('charizard'),
    buscarPokemon('bulbasaur')
]);

Apresente os três cards.


Exercício 7 — Estado de carregamento

Adicione uma mensagem:

Carregando...

enquanto as consultas estiverem sendo realizadas.


Exercício 8 — Transferência de aprendizagem

Substitua a PokéAPI por outra API pública.

A nova aplicação deverá manter a mesma arquitetura:

Evento
 ↓
Função async
 ↓
fetch()
 ↓
Response
 ↓
JSON
 ↓
Promise
 ↓
Promise.all()
 ↓
Dados
 ↓
DOM

O domínio deverá ser diferente de Pokémon.


DESAFIO DE RACIOCÍNIO

Considere:

const [
    pokemon1,
    pokemon2
] = await Promise.all([
    buscarPokemon('pikachu'),
    buscarPokemon('charizard')
]);

Responda às questões:

1. Quantas Promises estão sendo fornecidas ao Promise.all()?

2. Qual operação corresponde a pokemon1?

3. Qual operação corresponde a pokemon2?

4. O que acontece se a consulta de Pikachu terminar antes da consulta de Charizard?

5. O que acontece se a consulta de Charizard terminar antes da consulta de Pikachu?

6. O que acontece se uma das duas consultas falhar?

7. Por que utilizar Promise.all() nesse caso?

8. Qual é a diferença entre:

await buscarPokemon('pikachu');

e:

await Promise.all([
    buscarPokemon('pikachu'),
    buscarPokemon('charizard')
]);

Questão final

Explique, com suas próprias palavras:

Por que Promise.all() é adequado quando temos várias operações assíncronas independentes que precisam ser concluídas antes de continuar?


O QUE APRENDEMOS

Síntese da Aula

Nesta aula, avançamos de uma única requisição para múltiplas requisições assíncronas.

Aprendemos que:

  • Uma aplicação pode realizar várias operações assíncronas independentes;
  • Duas consultas independentes não precisam ser iniciadas sequencialmente;
  • Promise.all() permite aguardar um conjunto de Promises;
  • Os resultados permanecem associados à ordem das Promises fornecidas;
  • Se uma Promise rejeitar, o Promise.all() também será rejeitado;
  • try/catch pode tratar essa falha;
  • Os resultados de uma API podem ser utilizados para construir elementos da interface;
  • Estados de carregamento melhoram a experiência do usuário;
  • Erros devem ser tratados tanto tecnicamente quanto visualmente;
  • Uma aplicação Web assíncrona combina eventos, funções, Promises, async/await, APIs, JSON e DOM.

A arquitetura final estudada foi:

EVENTO
  ↓
FUNÇÃO ASSÍNCRONA
  ↓
FETCH
  ↓
RESPONSE
  ↓
JSON
  ↓
PROMISE
  ↓
PROMISE.ALL()
  ↓
DADOS
  ↓
RENDERIZAÇÃO
  ↓
DOM
  ↓
INTERFACE

A Pokédex é, portanto, mais do que um exercício sobre Pokémon.

Ela representa um modelo de aplicação Web capaz de:

solicitar dados externos, aguardar operações assíncronas, tratar resultados e erros e transformar dados recebidos em uma interface dinâmica.


EXERCÍCIO PARA CASA

Exercício para casa — Uma aplicação com duas consultas

Crie uma aplicação Web que permita ao usuário escolher dois recursos de uma API pública.

A aplicação deverá:

  1. Possuir dois campos de seleção ou entrada;
  2. Possuir um botão para iniciar a consulta;
  3. Utilizar uma função async;
  4. Utilizar fetch();
  5. Utilizar response.ok;
  6. Utilizar response.json();
  7. Utilizar Promise.all();
  8. Utilizar try/catch;
  9. Apresentar uma mensagem de carregamento;
  10. Apresentar os resultados na página;
  11. Apresentar uma mensagem caso ocorra erro.

Requisito principal

As duas consultas deverão ser independentes.

A solução deverá utilizar:

Promise.all([
    primeiraConsulta,
    segundaConsulta
]);

Desafio

Não utilize a PokéAPI.

Escolha outro domínio.

Exemplos:

  • Filmes;
  • Países;
  • Produtos;
  • Livros;
  • Jogos;
  • Astronomia;
  • Notícias;
  • Dados meteorológicos.

A estrutura da aplicação deverá continuar sendo:

Usuário
 ↓
Evento
 ↓
async function
 ↓
fetch()
 ↓
Promise.all()
 ↓
JSON
 ↓
Dados
 ↓
DOM

Reflexão final

Escreva um pequeno texto respondendo:

O que muda no desenvolvimento de uma aplicação quando os dados deixam de estar disponíveis localmente e passam a depender de um servidor externo?


REFERÊNCIAS

MDN Web Docs. JavaScript assíncrono — Promises.

MDN Web Docs. Promise.

MDN Web Docs. async function.

WHATWG. Fetch Standard.

PokéAPI. Documentation.

Mozilla Developer Network — MDN Web Docs. JavaScript assíncrono. Documentação oficial.

Mozilla Developer Network — MDN Web Docs. Promises. Documentação oficial.

Mozilla Developer Network — MDN Web Docs. Fetch API. Documentação oficial.

WHATWG. Fetch Standard.

PokéAPI. Documentação oficial da API.

Material anterior da disciplina: Linguagem e Técnica de Programação II — Aula 03, utilizado como pré-requisito para esta unidade. O material anterior estabelece que o domínio de JavaScript, funções, DOM, eventos e formulários constitui a base para avançar para consumo de dados e programação assíncrona. O material da disciplina recomenda a progressão conceitual de programação síncrona para assincronismo, callbacks, Promises, then/catch, async/await e, posteriormente, fetch().

 

Click to listen highlighted text!