O autor do artigo examina exemplos de Async/Await em JavaScript. No geral, Async/Await é uma maneira conveniente de escrever código assíncrono. Antes da introdução dessa funcionalidade, esse tipo de código era escrito usando callbacks e promessas. O autor do artigo original revela as vantagens do Async/Await, analisando diferentes exemplos.
Lembramos: para todos os leitores do «Habr» — desconto de 10.000 rublos na inscrição em qualquer curso Skillbox com o código promocional «Habr».
A Skillbox recomenda: Curso online educacional .
Callback
Um Callback é uma função cujo chamado é adiado por um período indefinido. Antigamente, callbacks eram usados nas partes do código onde o resultado não poderia ser obtido imediatamente.
Aqui está um exemplo de leitura assíncrona de um arquivo no Node.js:
fs.readFile(__filename, 'utf-8', (err, data) => {
if (err) {
throw err;
}
console.log(data);
}); Os problemas surgem no momento em que é necessário executar várias operações assíncronas ao mesmo tempo. Vamos imaginar o seguinte cenário: uma consulta ao banco de dados do usuário Arfat, é necessário ler o campo profile_img_url e carregar a imagem do servidor someserver.com.
Após o upload, convertemos a imagem para outro formato, por exemplo, de PNG para JPEG. Se a conversão foi bem-sucedida, um e-mail é enviado para o usuário. Em seguida, as informações sobre o evento são registradas no arquivo transformations.log com a data.
Vale a pena notar a sobreposição de callbacks e a grande quantidade de }) na parte final do código. Isso é chamado de Callback Hell ou Pyramid of Doom.
As desvantagens desse método são evidentes:
- Esse código é difícil de ler.
- Também é complicado tratar erros, o que muitas vezes leva a uma redução na qualidade do código.
Para resolver esse problema, promessas foram adicionadas ao JavaScript. Elas permitem substituir a profunda aninhamento de callbacks pela palavra .then.
Um aspecto positivo das promessas é que o código se torna muito mais legível, lido de cima para baixo, e não da esquerda para a direita. No entanto, as promessas também têm seus próprios problemas:
- É necessário adicionar um grande número de .then.
- Em vez de try/catch, utiliza-se .catch para tratar todos os erros.
- Trabalhar com várias promessas dentro de um único ciclo nem sempre é conveniente, e em alguns casos, isso complica o código.
Aqui está um problema que ilustrará a importância do último ponto.
Suponha que exista um loop for que imprime uma sequência de números de 0 a 10 com um intervalo aleatório (0–n segundos). Usando promessas, é necessário modificar esse loop para que os números sejam exibidos na sequência de 0 a 10. Assim, se a saída de zero leva 6 segundos e a de um leva 2 segundos, primeiro deve ser exibido o zero, e então começará a contagem da saída de um.
E claro, para resolver essa tarefa, não usamos Async/Await ou .sort. Um exemplo de solução está no final.
Funções Async
A adição de funções async no ES2017 (ES8) simplificou o trabalho com promessas. Vale ressaltar que as funções async operam 'em cima' das promessas. Essas funções não representam conceitos qualitativamente diferentes. As funções async foram criadas como uma alternativa ao código que utiliza promessas.
Async/Await permite organizar o trabalho com código assíncrono em um estilo síncrono.
Assim, o conhecimento sobre promessas facilita a compreensão dos princípios do Async/Await.
Sintaxe
Em situação normal, consiste em duas palavras-chave: async e await. A primeira palavra transforma a função em assíncrona. Nessas funções, o uso de await é permitido. Em qualquer outro caso, usar essa função resultará em um erro.
// With function declaration
async function myFn() {
// await ...
}
// With arrow function
const myFn = async () => {
// await ...
}
function myFn() {
// await fn(); (Syntax Error since no async)
}
Async é inserido no início da declaração da função, e no caso de uso de uma função de flecha, entre o sinal '=' e os parênteses.
Essas funções podem ser colocadas em um objeto como métodos ou utilizadas na declaração de uma classe.
// As an object's method
const obj = {
async getName() {
return fetch('https://www.example.com');
}
}
// In a class
class Obj {
async getResource() {
return fetch('https://www.example.com');
}
}NB! É importante lembrar que construtores de classe e getters/setters não podem ser assíncronos.
Semântica e regras de execução
As funções Async, em princípio, são semelhantes às funções padrão do JS, mas existem exceções.
Dessa forma, as funções async sempre retornam promessas:
async function fn() {
return 'hello';
}
fn().then(console.log)
// helloEm particular, fn retorna a string hello. E como esta é uma função assíncrona, o valor da string é encapsulado em uma promessa usando o construtor.
Aqui está uma construção alternativa sem Async:
function fn() {
return Promise.resolve('hello');
}
fn().then(console.log);
// helloNesse caso, o retorno da promessa é feito 'manualmente'. A função assíncrona sempre é encapsulada em uma nova promessa.
Se o valor de retorno for primitivo, a função async faz a devolução do valor, envolvendo-o em uma promessa. Se o valor de retorno for um objeto de promessa, sua resolução é retornada em uma nova promessa.
const p = Promise.resolve('hello')
p instanceof Promise;
// true
Promise.resolve(p) === p;
// true
Mas o que acontece se ocorrer um erro dentro da função assíncrona?
async function foo() {
throw Error('bar');
}
foo().catch(console.log);Se não for tratado, foo() retornará uma promessa rejeitada. Nessa situação, em vez de Promise.resolve, retornará Promise.reject, contendo o erro.
Funções assíncronas sempre retornam uma promessa, independentemente do que é retornado.
Funções assíncronas são suspensas em cada await.
Await afeta expressões. Se a expressão for uma promessa, a função async é suspensa até que a promessa seja resolvida. Se a expressão não for uma promessa, ela é convertida em promessa através de Promise.resolve e, em seguida, finalizada.
// utility function to cause delay
// and get random value
const delayAndGetRandom = (ms) => {
return new Promise(resolve => setTimeout(
() => {
const val = Math.trunc(Math.random() * 100);
resolve(val);
}, ms
));
};
async function fn() {
const a = await 9;
const b = await delayAndGetRandom(1000);
const c = await 5;
await delayAndGetRandom(1000);
return a + b * c;
}
// Execute fn
fn().then(console.log);Aqui está a descrição de como a função fn funciona.
- Após sua chamada, a primeira linha é convertida de const a = await 9; para const a = await Promise.resolve(9);.
- Após o uso de Await, a execução da função é suspensa até que a variável a obtenha seu valor (nesta situação, é 9).
- delayAndGetRandom(1000) suspende a execução da função fn até que ela própria seja concluída (após 1 segundo). Isso é, na prática, parar a função fn por 1 segundo.
- delayAndGetRandom(1000) retorna um valor aleatório através de resolve, que é então atribuído à variável b.
- Já o caso da variável c é semelhante ao da variável a. Após isso, tudo é pausado por um segundo, mas agora delayAndGetRandom(1000) não retorna nada, pois não é necessário.
- No final, os valores são calculados pela fórmula a + b * c. O resultado é envolto em uma promessa usando Promise.resolve e retornado pela função.
Essas pausas podem lembrar geradores no ES6, mas isso tem .
Resolvendo a tarefa
Agora vamos analisar a solução da tarefa mencionada acima.
Na função finishMyTask, usa-se Await para aguardar os resultados de operações como queryDatabase, sendEmail, logTaskInFile e outras. Se compararmos essa solução com a que usava promessas, a semelhança torna-se evidente. No entanto, a versão com Async/Await simplifica bastante todas as complexidades de sintaxe. Nesse caso, não há uma grande quantidade de callbacks e cadeias como .then/.catch.
Aqui está uma solução que exibe números, com duas opções disponíveis.
const wait = (i, ms) => new Promise(resolve => setTimeout(() => resolve(i), ms));
// Implementation One (Using for-loop)
const printNumbers = () => new Promise((resolve) => {
let pr = Promise.resolve(0);
for (let i = 1; i <= 10; i += 1) {
pr = pr.then((val) => {
console.log(val);
return wait(i, Math.random() * 1000);
});
}
resolve(pr);
});
// Implementation Two (Using Recursion)
const printNumbersRecursive = () => {
return Promise.resolve(0).then(function processNextPromise(i) {
if (i === 10) {
return undefined;
}
return wait(i, Math.random() * 1000).then((val) => {
console.log(val);
return processNextPromise(i + 1);
});
});
};E aqui está uma solução utilizando funções assíncronas.
async function printNumbersUsingAsync() {
for (let i = 0; i < 10; i++) {
await wait(i, Math.random() * 1000);
console.log(i);
}
}Manipulação de erros
Erros não tratados são encapsulados em promessas rejeitadas. No entanto, em funções assíncronas, pode-se usar a estrutura try/catch para realizar o tratamento de erros de forma síncrona.
async function canRejectOrReturn() {
// aguarda um segundo
await new Promise(res => setTimeout(res, 1000));
// Rejeita com ~50% de probabilidade
if (Math.random() > 0.5) {
throw new Error('Desculpe, número muito grande.')
}
return 'número perfeito';
}canRejectOrReturn() é uma função assíncrona que pode ser executada com sucesso (“número perfeito”) ou terminar com erro (“Desculpe, número muito grande”).
async function foo() {
try {
await canRejectOrReturn();
} catch (e) {
return 'erro capturado';
}
}Como no exemplo acima espera-se a execução de canRejectOrReturn, uma falha resultará na execução do bloco catch. Portanto, a função foo terminará com undefined (quando nada é retornado no bloco try) ou com erro capturado. Assim, essa função não terá uma falha, pois o try/catch lidará com o erro da própria função foo.
Aqui está mais um exemplo:
async function foo() {
try {
return canRejectOrReturn();
} catch (e) {
return 'erro capturado';
}
}É importante observar que na exemplo de foo retorna-se canRejectOrReturn. Neste caso, foo termina com número perfeito, ou retorna um erro Error (“Desculpe, número muito grande”). O bloco catch nunca será executado.
O problema é que foo retorna a promessa passada de canRejectOrReturn. Portanto, a solução da função foo se torna a solução para canRejectOrReturn. Neste caso, o código consistirá de apenas duas linhas:
try {
const promise = canRejectOrReturn();
return promise;
}E aqui está o que acontecerá se usarmos await e return juntos:
async function foo() {
try {
return await canRejectOrReturn();
} catch (e) {
return 'erro capturado';
}
}No código acima, foo pode ser concluída com sucesso tanto com número perfeito quanto com erro capturado. Aqui não haverá falhas. Mas foo terminará com canRejectOrReturn, e não com undefined. Vamos verificar isso, removendo a linha return await canRejectOrReturn():
try {
const value = await canRejectOrReturn();
return value;
}
// …Erros comuns e armadilhas
Em alguns casos, o uso de Async/Await pode levar a erros.
Await esquecido
Isso acontece com bastante frequência — a palavra-chave await é esquecida antes da promessa:
async function foo() {
try {
canRejectOrReturn();
} catch (e) {
return 'capturado';
}
}No código, como pode ser visto, não há nem await nem return. Portanto, foo sempre termina com undefined sem uma pausa de 1 segundo. Mas a promessa será executada. Se, por acaso, ela gerar um erro ou for rejeitada, neste caso, será chamada a UnhandledPromiseRejectionWarning.
Funções Async em callbacks
Funções Async são frequentemente usadas em .map ou .filter como callbacks. Um exemplo disso é a função fetchPublicReposCount(username), que retorna o número de repositórios públicos no GitHub. Suponha que existam três usuários cujas métricas precisamos. Aqui está o código para essa tarefa:
const url = 'https://api.github.com/users';
// Função utilitária para buscar contagens de repositórios
const fetchPublicReposCount = async (username) => {
const response = await fetch(`${url}/${username}`);
const json = await response.json();
return json['public_repos'];
}Precisamos das contas ArfatSalman, octocat, norvig. Nesse caso, executamos:
const users = [
'ArfatSalman',
'octocat',
'norvig'
];
const counts = users.map(async username => {
const count = await fetchPublicReposCount(username);
return count;
});É importante notar o Await no callback .map. Aqui, counts é um array de promessas, e .map é um callback anônimo para cada usuário especificado.
Uso excessivo de await de forma sequencial
Como exemplo, pegue o seguinte código:
async function fetchAllCounts(users) {
const counts = [];
for (let i = 0; i < users.length; i++) {
const username = users[i];
const count = await fetchPublicReposCount(username);
counts.push(count);
}
return counts;
}Aqui, a variável count armazena o número de repositórios, que é então adicionado ao array counts. O problema do código é que, enquanto os dados do primeiro usuário não chegarem do servidor, todos os usuários subsequentes ficarão em modo de espera. Dessa forma, apenas um usuário é processado de cada vez.
Se, por exemplo, levar cerca de 300 ms para processar um único usuário, para todos os usuários isso já é um segundo; o tempo gasto depende linearmente do número de usuários. Mas como a obtenção da contagem de repositórios não depende uma da outra, os processos podem ser paralelizados. Para isso, precisamos trabalhar com .map e Promise.all:
async function fetchAllCounts(users) {
const promises = users.map(async username => {
const count = await fetchPublicReposCount(username);
return count;
});
return Promise.all(promises);
}Promise.all recebe um array de promessas, retornando uma promessa. Finaliza quando todas as promessas no array são resolvidas ou na primeira rejeição. Pode acontecer que nem todas sejam iniciadas ao mesmo tempo; para garantir a execução simultânea, pode-se usar p-map.
Conclusão
Funções assíncronas estão se tornando cada vez mais importantes para o desenvolvimento. Para um uso adaptável de funções assíncronas, vale a pena considerar . Um desenvolvedor JavaScript deve ter um bom entendimento sobre isso.
A Skillbox recomenda:
- Curso Prático .
- Curso Online Aplicado .
- Curso Prático de Dois Anos .
Fonte: habr.com
