Vejamos Async/Await em JavaScript usando exemplos

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 desse recurso aparecer, esse código era escrito usando retornos de chamada e promessas. O autor do artigo original revela as vantagens do Async/Await analisando vários exemplos.

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

A Skillbox recomenda: Curso educacional on-line "Desenvolvedor de Java".

Callback

Callback é uma função cuja chamada é atrasada indefinidamente. Anteriormente, os retornos de chamada eram usados ​​nas áreas do código onde o resultado não podia ser obtido imediatamente.

Aqui está um exemplo de leitura assíncrona de um arquivo em Node.js:

fs.readFile(__filename, 'utf-8', (err, data) => {
  if (err) {
    throw err;
  }
  console.log(data);
});

Os problemas surgem quando você precisa realizar várias operações assíncronas ao mesmo tempo. Vamos imaginar este cenário: é feita uma solicitação ao banco de dados do usuário Arfat, é necessário ler seu campo profile_img_url e baixar uma imagem do servidor someserver.com.
Após o download, convertemos a imagem para outro formato, por exemplo de PNG para JPEG. Se a conversão for bem-sucedida, uma carta será enviada para o e-mail do usuário. A seguir, as informações sobre o evento são inseridas no arquivo transforms.log, indicando a data.

Vale atentar para a sobreposição de callbacks e o grande número de }) na parte final do código. Chama-se 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 muitas vezes leva à baixa qualidade do código.

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

O aspecto positivo das promessas é que elas tornam o código muito mais legível, de cima para baixo, e não da esquerda para a direita. No entanto, as promessas também têm os seus problemas:

  • Você precisa adicionar muito .then.
  • Em vez de try/catch, .catch é usado para lidar com todos os erros.
  • Trabalhar com múltiplas promessas dentro de um loop nem sempre é conveniente; em alguns casos, elas complicam o código.

Aqui está um problema que mostrará o significado do último ponto.

Suponha que temos um loop for que imprime uma sequência de números de 0 a 10 em intervalos aleatórios (0–n segundos). Usando promessas, você precisa alterar esse loop para que os números sejam impressos em sequência de 0 a 10. Portanto, se leva 6 segundos para imprimir um zero e 2 segundos para imprimir um um, o zero deve ser impresso primeiro e depois a contagem regressiva para impressão começará.

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

Funções assíncronas

A adição de funções assíncronas no ES2017 (ES8) simplificou a tarefa de trabalhar com promessas. Observo que as funções assíncronas funcionam “em cima” das promessas. Estas funções não representam conceitos qualitativamente diferentes. As funções assíncronas pretendem ser uma alternativa ao código que usa promessas.

Async/Await possibilita organizar o trabalho com código assíncrono em um estilo síncrono.

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

sintaxe

Normalmente consiste em duas palavras-chave: async e await. A primeira palavra transforma a função em assíncrona. Tais funções permitem o uso de await. Em qualquer outro caso, usar esta função irá gerar 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 logo no início da declaração da função e, no caso de uma função de seta, entre o sinal “=” e os parênteses.

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');
  }
}

Atenção! Vale lembrar que construtores de classes 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.

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

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

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

Aqui está um design alternativo sem Async:

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

Neste caso, a promessa é devolvida “manualmente”. Uma função assíncrona está sempre envolvida em uma nova promessa.

Se o valor de retorno for primitivo, a função assíncrona retorna o valor envolvendo-o em uma promessa. Se o valor retornado for um objeto de promessa, sua resolução será retornada em uma nova promessa.

const p = Promise.resolve('hello')
p instanceof Promise;
// true
 
Promise.resolve(p) === p;
// true
 

Mas o que acontece se houver um erro dentro de uma função assíncrona?

async function foo() {
  throw Error('bar');
}
 
foo().catch(console.log);

Se não for processado, foo() retornará uma promessa com rejeição. Nessa situação, Promise.reject contendo um erro será retornado em vez de Promise.resolve.

As funções assíncronas sempre geram uma promessa, independentemente do que é retornado.

Funções assíncronas pausam a cada await .

Await afeta expressões. Portanto, se a expressão for uma promessa, a função assíncrona será suspensa até que a promessa seja cumprida. Se a expressão não for uma promessa, ela será convertida em uma promessa por meio de Promise.resolve e então concluída.

// 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);

E aqui está uma descrição de como funciona a função fn.

  • Após chamá-lo, a primeira linha é convertida de const a = await 9; em const a = aguardar Promise.resolve(9);.
  • Após usar Await, a execução da função é suspensa até que a obtenha seu valor (na situação atual é 9).
  • delayAndGetRandom(1000) pausa a execução da função fn até que ela seja concluída (após 1 segundo). Isso interrompe efetivamente a função fn por 1 segundo.
  • delayAndGetRandom(1000) via resolve 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 para por um segundo, mas agora delayAndGetRandom(1000) não retorna nada porque não é obrigató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 lembrar os geradores do ES6, mas há algo nisso suas razões.

Resolvendo o problema

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

A função finishMyTask usa Await para aguardar os resultados de operações como queryDatabase, sendEmail, logTaskInFile e outras. Se compararmos esta solução com aquela em que foram utilizadas promessas, as semelhanças tornar-se-ão óbvias. No entanto, a versão Async/Await simplifica muito todas as complexidades sintáticas. Nesse caso, não há um grande número de retornos de chamada e cadeias como .then/.catch.

Aqui está uma solução com 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);
  }
}

Erro ao processar

Erros não tratados são envoltos em uma promessa rejeitada. No entanto, funções assíncronas podem usar try/catch para tratar erros de forma 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 tem 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 o exemplo acima espera que canRejectOrReturn seja executado, sua própria falha resultará na execução do bloco catch. Como resultado, a função foo terminará com indefinido (quando nada for retornado no bloco try) ou com um erro detectado. Como resultado, esta função não falhará porque o try/catch tratará a própria função foo.

Aqui está outro exemplo:

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

Vale atentar para o fato de que no exemplo canRejectOrReturn é retornado de foo. Foo, neste caso, termina com um número perfeito ou retorna um Erro (“Desculpe, o número é muito grande”). O bloco catch nunca será executado.

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

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

Aqui está o que acontece se você usar wait e return juntos:

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

No código acima, foo sairá com sucesso com um número perfeito e um erro detectado. Não haverá recusas aqui. Mas foo retornará com canRejectOrReturn, não com indefinido. Vamos ter certeza disso removendo a linha return await canRejectOrReturn():

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

Erros e armadilhas comuns

Em alguns casos, usar Async/Await pode levar a erros.

Esquecidos aguardam

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

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

Como você pode ver, não há espera ou retorno no código. Portanto, foo sempre sai indefinido sem atraso de 1 segundo. Mas a promessa será cumprida. Se produzir um erro ou rejeição, UnhandledPromiseRejectionWarning será chamado.

Funções assíncronas em retornos de chamada

Funções assíncronas são frequentemente usadas em .map ou .filter como retornos de chamada. Um exemplo é a função fetchPublicReposCount(username), que retorna o número de repositórios abertos no GitHub. Digamos que haja 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. Neste caso fazemos:

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

Vale a pena prestar atenção em Await no retorno de chamada do .map. Aqui contagens é uma série de promessas e .map é um retorno de chamada anônimo para cada usuário especificado.

Uso excessivamente consistente de aguardar

Vamos pegar este 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 repositório é colocado na variável de contagem e, em seguida, esse número é adicionado ao array de contagens. O problema com o código é que até que os dados do primeiro usuário cheguem do servidor, todos os usuários subsequentes estarão no modo de espera. Assim, apenas um usuário é processado por vez.

Se, por exemplo, leva cerca de 300 ms para processar um usuário, então para todos os usuários já é um segundo; o tempo gasto depende linearmente do número de usuários. Mas como a obtenção do número de repo não depende um do outro, os processos podem ser paralelizados. Isso requer 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 uma série de promessas como entrada e retorna uma promessa. Este último, após todas as promessas do array terem sido concluídas ou na primeira rejeição, é concluído. Pode acontecer que todos eles não iniciem ao mesmo tempo - para garantir a inicialização simultânea, você pode usar o p-map.

Conclusão

As funções assíncronas estão se tornando cada vez mais importantes para o desenvolvimento. Bem, para uso adaptativo de funções assíncronas, você deve usar Iteradores assíncronos. Um desenvolvedor JavaScript deve ser bem versado nisso.

A Skillbox recomenda:

Fonte: habr.com

Adicionar um comentário