Analisando Async / Await em JavaScript com exemplos



O autor do artigo analisa Async / Await em JavaScript usando exemplos. Em geral, o Async / Await é uma maneira conveniente de escrever código assíncrono. Antes dessa oportunidade, um código semelhante era escrito usando retornos de chamada e promessas. O autor do artigo original revela os benefícios do Async / Await examinando vários exemplos.

Lembramos que: para todos os leitores de "Habr" - um desconto de 10.000 rublos ao se inscrever em qualquer curso Skillbox usando o código promocional "Habr".

A Skillbox recomenda: O Curso de Educação Online para Desenvolvedores Java .

Retorno de chamada


O retorno de chamada é uma função cuja chamada está atrasada indefinidamente. Anteriormente, os retornos de chamada eram usados ​​nas partes do código em que o resultado não podia 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 quando você precisa executar várias operações assíncronas ao mesmo tempo. Vamos imaginar este cenário: uma solicitação é feita ao banco de dados do usuário da Arfat, você precisa ler o campo profile_img_url e baixar uma imagem do servidor someserver.com.
Após o download, converta a imagem em outro formato, por exemplo, de PNG para JPEG. Se a conversão foi bem-sucedida, um email é enviado para o email do usuário. Além disso, as informações sobre o evento são inseridas no arquivo transformations.log com a data.



Vale a pena prestar atenção à imposição de retornos de chamada e um grande número}) na parte final do código. Isso é chamado de Callback Hell ou Pyramid of Doom.

As desvantagens deste método são óbvias:

  • Este código é difícil de ler.
  • Também é difícil lidar com erros, o que geralmente leva a uma deterioração na qualidade do código.

Para resolver esse problema, foram adicionadas promessas ao JavaScript. Eles permitem que você substitua o aninhamento profundo de retornos de chamada pela palavra .then.



O ponto positivo das promessas foi que, com elas, o código é lido muito melhor, de cima para baixo, e não da esquerda para a direita. No entanto, as promessas também têm seus problemas:

  • Precisa adicionar uma grande quantidade de .then.
  • Em vez de try / catch, o .catch é usado para manipular todos os erros.
  • Trabalhar com várias promessas em um ciclo está longe de ser sempre conveniente; em alguns casos, elas complicam o código.

Aqui está uma tarefa que mostrará o significado do último parágrafo.

Suponha que exista um loop for que imprima uma sequência de números de 0 a 10 com um intervalo aleatório (0 - n segundos). Usando promessas, você precisa alterar esse ciclo para que os números sejam exibidos em uma sequência de 0 a 10. Portanto, se a saída zero demorar 6 segundos e as unidades demorar 2 segundos, primeiro será necessário zero e então a contagem regressiva será iniciada.

E, claro, para resolver esse problema, não usamos Async / Await ou .sort. Um exemplo de solução está no final.

Funções assíncronas


A adição de funções assíncronas ao ES2017 (ES8) simplificou a tarefa de trabalhar com promessas. Noto que as funções assíncronas funcionam além das promessas. Essas funções não representam conceitos qualitativamente diferentes. As funções assíncronas foram concebidas como uma alternativa ao código que usa promessas.

O Async / Await torna possível organizar o trabalho com código assíncrono em um estilo síncrono.

Assim, o conhecimento das promessas facilita a compreensão dos princípios do Async / Await.

Sintaxe

Em uma situação típica, consiste em duas palavras-chave: assíncrona e aguardar. A primeira palavra torna a função assíncrona. Essas funções permitem aguardar. Em qualquer outro caso, o uso desta função causará 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) } 

O Async é inserido no início da declaração da função e, no caso da função de seta, entre o sinal "=" e os colchetes.

Essas funções podem ser colocadas em um objeto como métodos ou usadas em uma declaração de 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! Vale lembrar que construtores de classe e getters / setters não podem ser assíncronos.

Semântica e regras de execução

As funções assíncronas são basicamente semelhantes às funções JS padrão, mas há exceções.

Portanto, as funções assíncronas sempre retornam promessas:

 async function fn() { return 'hello'; } fn().then(console.log) // hello 

Em particular, fn retorna a string hello. Bem, como essa é uma função assíncrona, o valor da string é envolvido em uma promessa usando o construtor.

Aqui está um design alternativo sem o Async:

 function fn() { return Promise.resolve('hello'); } fn().then(console.log); // hello 

Nesse caso, o retorno da promessa é feito "manualmente". Uma função assíncrona sempre se envolve em uma nova promessa.

No caso de o valor de retorno ser primitivo, a função assíncrona retorna um valor, envolvendo-o em uma promessa. Caso o valor de retorno seja o objeto da promessa, sua solução será retornada na 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 processado, foo () retornará uma promessa com um redirecionamento. Nessa situação, em vez de Promise.resolve, Promise.reject retornará contendo um erro.

As funções assíncronas na saída sempre prometem, independentemente do que é retornado.

Funções assíncronas são pausadas a cada espera.

Aguardar afeta expressões. Portanto, se a expressão for uma promessa, a função assíncrona será suspensa até que a promessa seja executada. No caso de a expressão não ser uma promessa, ela é convertida em promessa através de Promise.resolve e, em seguida, é encerrada.

 // 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á uma descrição de como a função fn funciona.

  • Após chamá-lo, a primeira linha é convertida de const a = waiting 9; em const a = aguarde Promise.resolve (9);
  • Depois de usar Await, a execução da função é suspensa até receber seu valor (na situação atual, é 9).
  • delayAndGetRandom (1000) pausa a execução da função fn até que ela seja finalizada (após 1 segundo). Na verdade, isso está parando a função fn por 1 segundo.
  • delayAndGetRandom (1000) através da resolução retorna um valor aleatório, que é então atribuído à variável b.
  • Bem, o caso da variável c é semelhante ao caso da variável a. Depois disso, tudo pára por um segundo, mas agora delayAndGetRandom (1000) não retorna nada, pois isso não é necessário.
  • Como resultado, os valores são calculados pela fórmula a + b * c. O resultado é envolvido em uma promessa usando Promise.resolve e retornado pela função

Essas pausas podem se parecer com geradores no ES6, mas existem razões para isso .

Resolvemos o problema


Bem, agora vamos olhar a solução para o problema mencionado acima.



A função finishMyTask usa Aguardar para aguardar os resultados de operações como queryDatabase, sendEmail, logTaskInFile e outros. Se compararmos essa decisão com o local onde as promessas foram usadas, as semelhanças se tornarão aparentes. No entanto, a versão com Async / Await simplifica bastante todas as dificuldades sintáticas. Nesse caso, não há muitos retornos de chamada e cadeias como .then / .catch.

Aqui está uma solução com a saída de números, existem duas opções.

 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 usando funções assíncronas.

 async function printNumbersUsingAsync() { for (let i = 0; i < 10; i++) { await wait(i, Math.random() * 1000); console.log(i); } } 

Tratamento de erros

Erros não processados ​​são envolvidos em promessas rejeitadas. No entanto, nas funções assíncronas, você pode usar a construção try / catch para executar a manipulação de erros síncrona.

 async function canRejectOrReturn() { // wait one second await new Promise(res => setTimeout(res, 1000)); // Reject with ~50% probability if (Math.random() > 0.5) { throw new Error('Sorry, number too big.') } return 'perfect number'; } 

canRejectOrReturn () é uma função assíncrona que obtém sucesso ("número perfeito") ou falha com um erro ("Desculpe, número muito grande").

 async function foo() { try { await canRejectOrReturn(); } catch (e) { return 'error caught'; } } 

Como se espera que canRejectOrReturn seja executado no exemplo acima, sua própria conclusão sem êxito implicará a execução do bloco catch. Como resultado, a função foo termina com indefinido (quando nada é retornado no bloco try) ou com erro detectado. Como resultado, essa função não falhará, pois o try / catch tratará da própria função foo.

Aqui está outro exemplo:

 async function foo() { try { return canRejectOrReturn(); } catch (e) { return 'error caught'; } } 

Vale a pena prestar atenção ao fato de que no exemplo de foo canRejectOrReturn é retornado. Nesse caso, o Foo é concluído com um número perfeito ou retorna um erro de erro ("Desculpe, número muito grande"). O bloco de captura nunca será executado.

O problema é que foo retorna a promessa passada de canRejectOrReturn. Portanto, a solução para a função foo se torna a solução para canRejectOrReturn. Nesse caso, o código consistirá em apenas duas linhas:

 try { const promise = canRejectOrReturn(); return promise; } 

Mas o que acontece se você usar aguardar e retornar juntos:

 async function foo() { try { return await canRejectOrReturn(); } catch (e) { return 'error caught'; } } 

No código acima, foo obtém sucesso com número perfeito e erro detectado. Não haverá falhas. Mas foo terminará com canRejectOrReturn, e não com indefinido. Vamos garantir isso removendo a linha de retorno aguardar canRejectOrReturn ():

 try { const value = await canRejectOrReturn(); return value; } // … 

Erros e armadilhas comuns


Em alguns casos, o uso de Async / Await pode resultar em erros.

Esquecido, aguarde

Isso acontece com bastante frequência - antes da promessa, a palavra-chave wait é esquecida:

 async function foo() { try { canRejectOrReturn(); } catch (e) { return 'caught'; } } 

No código, como você pode ver, não há espera nem retorno. Portanto, o foo sempre sai com indefinido sem demora de 1 segundo. Mas a promessa será cumprida. Se ocorrer um erro ou um redirecionamento, será chamado UnhandledPromiseRejectionWarning.

Funções assíncronas em retornos de chamada

As funções assíncronas são frequentemente usadas em .map ou .filter como retornos de chamada. Um exemplo é a função fetchPublicReposCount (nome de usuário), que retorna o número de repositórios abertos no GitHub. Digamos que há três usuários cujas métricas precisamos. Aqui está o código para esta tarefa:

 const url = 'https://api.github.com/users'; // Utility fn to fetch repo counts const fetchPublicReposCount = async (username) => { const response = await fetch(`${url}/${username}`); const json = await response.json(); return json['public_repos']; } 

Precisamos de contas ArfatSalman, octocat, norvig. Nesse caso, execute:

 const users = [ 'ArfatSalman', 'octocat', 'norvig' ]; const counts = users.map(async username => { const count = await fetchPublicReposCount(username); return count; }); 

Você deve prestar atenção a Aguardar no retorno de chamada .map. Aqui conta é uma série de promessas, bem. .Map é um retorno de chamada anônimo para cada usuário especificado.

Uso excessivamente consistente de aguardar

Tome o seguinte código como exemplo:

 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, o número do repo é colocado na variável count, então esse número é adicionado à matriz de contagens. O problema com o código é que, até que os primeiros dados do usuário cheguem do servidor, todos os usuários subseqüentes estarão no modo de espera. Assim, em um único momento, apenas um usuário é processado.

Se, por exemplo, são necessários cerca de 300 ms para processar um usuário, então para todos os usuários já é um segundo, o tempo gasto linearmente depende do número de usuários. Mas como o número de repositórios não depende um do outro, os processos podem ser paralelos. Isso requer trabalho 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 na entrada recebe uma série de promessas com o retorno da promessa. A última após a conclusão de todas as promessas na matriz ou no primeiro redirecionamento é concluída. Pode acontecer que todos eles não iniciem ao mesmo tempo - para garantir o lançamento simultâneo, você pode usar o p-map.

Conclusão


Recursos assíncronos estão se tornando cada vez mais importantes para o desenvolvimento. Bem, para uso adaptativo de funções assíncronas, vale a pena usar Iteradores Assíncronos . O desenvolvedor do JavaScript deve ser bem versado nisso.

A Skillbox recomenda:

Source: https://habr.com/ru/post/pt458950/


All Articles