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
.
method
indica o verbo HTTP usado;headers
permite colocar cabeçalhos personalizados;body
permite enviar informações no corpo da requisição.
Objeto response
Representa a resposta dada pelo recurso acessado.
ok
indica que o resultado é um sucesso (código entre 200 e 299);status
indica o código retornado (o código mais comum de sucesso é 200);statusText
indica um texto legível do código retornado;headers
indica o cabeçalho da resposta;body
indica o corpo da resposta.
Veja uma lista completa dos status HTTP aqui.
Objeto body
(ou result
)
Possui o conteúdo recebido. Podemos lê-lo de diversas maneiras:
.json()
converte um conteúdo JSON em objeto do JavaScript;.text()
lê o texto sem processá-lo;.blob()
permite receber um arquivo binário (como uma imagem, por exemplo).
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:
method
indica o método a ser utilizado, comoGET
ouPOST
.headers
contém um objeto cujas propriedades serão enviadas no cabeçalho da requisição.body
contém uma string ou campos de formulário enviados no corpo da 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.
.status
possui o código de status do retorno (ex.:404
);.statusText
possui a descrição textual do status de retorno (ex.:NOT FOUND
);.ok
étrue
se o resultado possui status de sucesso (entre 200 e 299, inclusive);.json()
obtém um objeto JavaScript equivalente ao conteúdo JSON recebido.
Exemplo:
// ...
const response = await fetch(url, requestOptions);
if (response.ok) {
// Sucesso
const result = await response.json();
// ...
} else {
// Erro
alert(`Erro: ${response.status} - ${response.statusText}`);
}
// ...