Entendendo e evitando comportamentos indesejádos
Usar o async/await ao criar loops em arrays no Javascript parece simples, mas há um comportamento não tão intuitivo a ser observado ao combinar os dois. Vamos dar uma olhada em três exemplos diferentes, para ver o que você deve prestar atenção e qual é o melhor para casos de uso específicos.
forEach
Se você puder tirar apenas uma coisa deste artigo, que seja: async/await não funciona em Array.prototype.forEach. Vamos ver um exemplo para ver o porquê:
const urls = [
'https://jsonplaceholder.typicode.com/todos/1',
'https://jsonplaceholder.typicode.com/todos/2',
'https://jsonplaceholder.typicode.com/todos/3'
];
async function getTodos() {
await urls.forEach(async (url, idx) => {
const todo = await fetch(url);
console.log(`Received Todo ${idx+1}:`, todo);
});
console.log('Finished!');
}
Resultado:
Finished!
Received Todo 2, Response: { ··· }
Received Todo 1, Response: { ··· }
Received Todo 3, Response: { ··· }
⚠️ Problema 1:
O código acima será executado com sucesso. No entanto, observe que Finished! foi mostrado primeiro, apesar do uso de await antes do urls.forEach. O primeiro problema é que você não pode fazer await no loop inteiro ao usar forEach.
⚠️ Problema 2:
Além disso, apesar do uso de await dentro do loop, ele não esperou que cada solicitação terminasse antes de executar a próxima. Então, os pedidos foram registrados fora de ordem. Se a primeira solicitação demorar mais que as solicitações a seguir, ela ainda poderá terminar por último.
Por ambas as razões, forEach não deve ser invocado se você estiver usando async/await.
Promise.all
Vamos resolver o problema de esperar que todo o loop seja concluído. Como await cria uma Promise sob o capô, podemos usar Promise.all com await para esperar todos os pedidos que foram iniciados durante o loop:
const urls = [
'https://jsonplaceholder.typicode.com/todos/1',
'https://jsonplaceholder.typicode.com/todos/2',
'https://jsonplaceholder.typicode.com/todos/3'
];
async function getTodos() {
const promises = urls.map(async (url, idx) =>
console.log(`Received Todo ${idx+1}:`, await fetch(url))
);
await Promise.all(promises);
console.log('Finished!');
}
Resultado:
Received Todo 1, Response: { ··· }
Received Todo 2, Response: { ··· }
Received Todo 3, Response: { ··· }
Finished!
Resolvemos a questão de esperar que todas as solicitações terminem antes de continuar. Parece também que resolvemos a questão dos pedidos que acontecem fora de ordem, mas não é exatamente esse o caso.
Como mencionado anteriormente, Promise.all irá esperar a execução de todas as promessas feitas em paralelo. Não iremos esperar que o primeiro pedido seja retornado antes de começar o segundo, ou o terceiro. Para a maioria das finalidades, isso é bom e é uma solução muito eficaz. Mas, se você realmente precisar que cada solicitação aconteça em ordem , Promise.all não resolverá isso.
for..of
Sabemos que forEach não respeita em nada o async/await e Promise.all só funciona se a ordem de execução não for importante. Vamos ver uma solução que resolve os dois casos.
O for..of executa o loop na ordem esperada - aguardando que cada operação await anterior seja concluída antes de passar para a próxima:
const urls = [
'https://jsonplaceholder.typicode.com/todos/1',
'https://jsonplaceholder.typicode.com/todos/2',
'https://jsonplaceholder.typicode.com/todos/3'
];
async function getTodos() {
for (const [idx, url] of urls.entries()) {
const todo = await fetch(url);
console.log(`Received Todo ${idx+1}:`, todo);
}
console.log('Finished!');
}
Resultado:
Received Todo 1, Response: { ··· }
Received Todo 2, Response: { ··· }
Received Todo 3, Response: { ··· }
Finished!
Eu particularmente gosto de como esse método permite que o código permaneça linear - o que é um dos principais benefícios de usar async/await. Eu acho muito mais fácil de ler do que as alternativas.
Se você não precisa acessar o índice, o código fica ainda mais conciso:
for (const url of urls) { ··· }
Uma das principais desvantagens de usar um loop for..of é que sua performance é baixa em comparação com as outras opções de loop em JavaScript. No entanto, o argumento de desempenho é insignificante quando usado em chamadas await assíncronas, uma vez que a intenção é manter o loop até que cada chamada seja resolvida. Eu normalmente só uso for..of` se a ordem de execução assíncrona for importante.
Nota: Você também pode usar loops for básicos para obter todos os benefícios de for..of, mas eu gosto da simplicidade e legibilidade que for..of oferece.
👏 Se você achou este artigo útil e gostaria de ver mais, por favor, comente abaixo ou deixei algumas palmas! 🔗 Fique ligado para mais artigos como este!
Atualização 21/08/2019: Baseado no comentário do Yukihiro Yamashita no Medium sobre funções recursivas. Tomei a liberdade e criei um exemplo de como fazer um “fetch recursivo”, lembre-se de criar uma função exaustiva para evitar loop infinito!
Créditos ⭐️
- The Pitfalls of Async/Await in Array Loops, escrito originalmente por Tory Walker
Top comments (1)
nice!