View on GitHub

aulas-programacao-web

Materiais de Aula - Programação de Computadores com tecnologias Web

Assincronia

📽 Veja esta vídeo-aula no Youtube

Devido à sua natureza conectada, nem todas as funções de JavaScript são executadas imediatamente ao serem chamadas. Funções que demoram um tempo grande ou indeterminado para executarem frequentemente são retiradas do fluxo (thread) normal, e colocadas para execução em paralelo. Chamamos essas funções de funções assíncronas.

Para tratar a assincronia usando recursos do JavaScript, existem várias técnicas. Vamos estudar o método baseado em Promises. Estudemos como exemplo de função assíncrona a Fetch API.

Fetch API

Permite a obtenção de recursos externos usando HTTP. É frequentemente utilizada para acessar recursos de backend, como APIs de aplicações e integração de sistemas.

Um exemplo é a API do GitHub, que permite interagir com o serviço. Por exemplo, a URL https://api.github.com/users/ermogenes é pública (não precisa de autenticação) e permite obter os dados do usuário indicado (@ermogenes) em formato JSON usando HTTP.

Você pode visualizar a URL diretamente no navegador. Seu resultado:

{
  "login": "ermogenes",
  "id": 14313064,
  "node_id": "MDQ6VXNlcjE0MzEzMDY0",
  "avatar_url": "https://avatars3.githubusercontent.com/u/14313064?v=4",
  "gravatar_id": "",
  "url": "https://api.github.com/users/ermogenes",
  "html_url": "https://github.com/ermogenes",
  "followers_url": "https://api.github.com/users/ermogenes/followers",
  "following_url": "https://api.github.com/users/ermogenes/following{/other_user}",
  "gists_url": "https://api.github.com/users/ermogenes/gists{/gist_id}",
  "starred_url": "https://api.github.com/users/ermogenes/starred{/owner}{/repo}",
  "subscriptions_url": "https://api.github.com/users/ermogenes/subscriptions",
  "organizations_url": "https://api.github.com/users/ermogenes/orgs",
  "repos_url": "https://api.github.com/users/ermogenes/repos",
  "events_url": "https://api.github.com/users/ermogenes/events{/privacy}",
  "received_events_url": "https://api.github.com/users/ermogenes/received_events",
  "type": "User",
  "site_admin": false,
  "name": "Ermogenes Palacio",
  "company": "Prodam",
  "blog": "http://ermogenes.github.io/",
  "location": "Santos, Brasil",
  "email": null,
  "hireable": null,
  "bio": "Dev @ Prodam. Teacher @ CPS/EtecAB. Azure AI Engineer Associate.",
  "twitter_username": "ermogenes",
  "public_repos": 80,
  "public_gists": 10,
  "followers": 39,
  "following": 33,
  "created_at": "2015-09-16T13:01:39Z",
  "updated_at": "2020-10-09T21:47:15Z"
}

Esse resultado é um objeto JSON (JavaScript Object Notation). Ele não é 100% igual a um objeto do JavaScript, mas sua sintaxe é baseada nele, portanto é facilmente convertível.

Podemos fazer essa mesma chamada HTTP usando fetch. Veja o exemplo:

const iniciar = () => {
  console.log('antes de fetch');

  const response = fetch('https://api.github.com/users/ermogenes');
  console.log(response);

  console.log('depois de fetch');
};

document.addEventListener('DOMContentLoaded', iniciar);

O resultado não parece muito com o esperado, mas na verdade há mais trabalho a fazer. Perceba que o resultado de fetch é uma Promise. Promises são estruturas de programação assíncrona que indicam que o resultado não estará disponível imediatamente, mas que permitem que se indiquem funções a serem executadas quando o resultado estiver disponível. Pense nela como uma promessa de executar algo quando a função for concluída.

Toda promise um método then, que indica a função a ser executada quando a promise for concluída. O método fetch retorna uma promise de que fará a conexão com o URL indicado e retornará a resposta.

const iniciar = () => {
  console.log('antes de fetch');

  fetch('https://api.github.com/users/ermogenes')
    .then((response) => console.log(response));

  console.log('depois de fetch');
};

document.addEventListener('DOMContentLoaded', iniciar);

Perceba que agora response só foi lida após a finalização da conexão. Porém, depois de fetch foi exibido normalmente, sem aguardar a função assíncrona.

Podemos fazê-la aguardar encadeando outro then, mesmo que o anterior não retorne explicitamente uma promise.

const iniciar = () => {
  console.log('antes de fetch');

  fetch('https://api.github.com/users/ermogenes')
    .then((response) => console.log(response))
    .then(() => console.log('depois de fetch'));
};

document.addEventListener('DOMContentLoaded', iniciar);

Perceba que o retorno obtido response ainda não contém o JSON que desejamos, e sim as informações de status sobre a obtenção deles. Obter o resultado exige outra promise.

const iniciar = () => {
  console.log('antes de fetch');

  fetch('https://api.github.com/users/ermogenes')
    .then((response) => response.json())
    .then((result) => console.log(result))
    .then(() => console.log('depois de fetch'));
};

document.addEventListener('DOMContentLoaded', iniciar);

É claro, podemos criar uma função que trata os resultados adequadamente. Como já foram convertidos de JSON para objetos do JavaScript, tudo que você já aprendeu é válido.

const exibeUsuario = (usuario) => {
  console.log(`O usuário ${usuario.login} possui ${usuario.public_repos} seguidores!`);
};

const iniciar = () => {
  fetch('https://api.github.com/users/ermogenes')
    .then((response) => response.json())
    .then((result) => exibeUsuario(result));
};

document.addEventListener('DOMContentLoaded', iniciar);

💩 Perceba que eu utilizei a informação public_repos (número de repositórios) quando deveria ter utilizado followers (seguidores). 😫

Async/Await

Uma sintaxe alternativa para fazer a mesma coisa é a chamada async/await. A palavra-chave await faz o papel do then, e async faz uma função automaticamente retornar uma promise.

Veja o mesmo exemplo, usando essa sintaxe:

const exibeUsuario = (usuario) => {
  console.log(`O usuário ${usuario.login} possui ${usuario.public_repos} seguidores!`);
};

const iniciar = () => {
  const response = await fetch('https://api.github.com/users/ermogenes');
  const result = await response.json();
  exibeUsuario(result);
};

document.addEventListener('DOMContentLoaded', iniciar);

Note que o exemplo gera um erro:

Sempre que utilizarmos await, a função atual deve ser marcada como assíncrona, para que retorne uma promise. Fazemos isso usando a palavra async.

const exibeUsuario = (usuario) => {
  console.log(`O usuário ${usuario.login} possui ${usuario.public_repos} seguidores!`);
};

const iniciar = async () => {
  const response = await fetch('https://api.github.com/users/ermogenes');
  const result = await response.json();
  exibeUsuario(result);
};

document.addEventListener('DOMContentLoaded', iniciar);

Agora tudo funciona como esperado.

Pense em await como uma maneira de esperar que uma função assíncrona termine antes de continuar o programa. Ela torna facilmente um comando assíncrono em síncrono.

Tratamento de erros

Além de then, toda promise também possui um catch, que indica a função a ser executada em caso de erro. Ele será executado se qualquer promise anterior a ele lançar uma exceção. Assim, precisamos alterar nossas funções para que elas retornem a próxima promise ou lancem uma exceção.

Veja o exemplo:

const exibeUsuario = (usuario) => {
  console.log(`O usuário ${usuario.login} possui ${usuario.public_repos} seguidores!`);
};

const iniciar = () => {
  console.log('buscando dados do usuário...');
  fetch('https://api.github.com/users/ermogenes')
    .then((response) => {
      if (response.ok) {
        return response.json()
      } else {
        throw new Error(`Erro ao acessar o URL: ${response.statusText}`);
      }
    })
    .then((result) => exibeUsuario(result))
    .catch((error) => console.log(`Um erro foi encontrado: ${error.message}`))
    .then(() => console.log('operação concluída.'));
};

document.addEventListener('DOMContentLoaded', iniciar);

Usando essa sintaxe não podemos utilizar try...catch. Se quisermos fazê-lo, devemos usar async/await. Veja um exemplo similar ao anterior.

const exibeUsuario = (usuario) => {
  console.log(`O usuário ${usuario.login} possui ${usuario.public_repos} seguidores!`);
};

const iniciar = async () => {
  console.log('buscando dados do usuário...');
  try {
    const response = await fetch('https://api.github.com/users/ermogenes');
    if (response.ok) {
      const result = await response.json();
      exibeUsuario(result);
    } else {
      throw new Error(`Erro ao acessar o URL: ${response.statusText}`);
    }
  } catch(error) {
    console.log(`Um erro foi encontrado: ${error.message}`)
  }
  console.log('operação concluída.');
};

document.addEventListener('DOMContentLoaded', iniciar);

Objeto request

Representa a sua requisição ao recurso. Você pode alterar a suas opções passando um segundo parâmetro para o fetch.

Objeto response

Representa a resposta dada pelo recurso acessado.

Veja uma lista completa dos status HTTP aqui.

Objeto body (ou result)

Possui o conteúdo recebido. Podemos lê-lo de diversas maneiras:

Fetch de APIs REST

Precisamos agora estudar um pouco mais aprofundadamente a Fetch API para que possamos consumir o nosso backend em todas as suas nuances.

A função fetch pode receber um segundo parâmetro indicando as opções da requisição (request): fetch(url, request). Esse objeto vai conter as configurações e os dados a serem enviados na requisição:

Chamadas REST

GET, sem parâmetro de query string:

// ...
fetch("/api/recurso")
// ...

GET, com parâmetro de query string:

// ...
fetch(`/api/recurso?parametro=${valorDesejado}`)
// ...

POST:

// ...
fetch("/api/recurso", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify(novoRecurso),
})
// ...

PUT:

// ...
fetch(`/api/recurso/${id}`, {
  method: "PUT",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify(recursoAlterado),
})
// ...

PATCH:

// ...
fetch(`/api/recurso/${id}/acao`, { method: "PATCH" })
// ...

DELETE:

// ...
fetch(`/api/recurso/${id}`, { method: "DELETE" })
// ...

Entendendo os resultados

Após a requisição, o objeto retornado possui todo o conteúdo da resposta.

Exemplo:

// ...
const response = await fetch(url, requestOptions);
if (response.ok) {
  // Sucesso
  const result = await response.json();
  // ...
} else {
  // Erro
  alert(`Erro: ${response.status} - ${response.statusText}`);
}
// ...