Vexamos Async/Await en JavaScript usando exemplos

O autor do artigo examina exemplos de Async/Await en JavaScript. En xeral, Async/Await é un xeito cómodo de escribir código asíncrono. Antes de que aparecese esta función, ese código escribíase usando devolucións de chamada e promesas. O autor do artigo orixinal revela as vantaxes de Async/Await analizando varios exemplos.

Recordámolo: para todos os lectores de "Habr" - un desconto de 10 rublos ao inscribirse en calquera curso de Skillbox usando o código promocional "Habr".

Skillbox recomenda: Curso educativo online "Desenvolvedor Java".

Callback

A devolución de chamada é unha función cuxa chamada se atrasa indefinidamente. Anteriormente, as devolucións de chamada usábanse naquelas áreas de código onde o resultado non se podía obter inmediatamente.

Aquí tes un exemplo de lectura asíncrona dun ficheiro en Node.js:

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

Os problemas xorden cando precisa realizar varias operacións asíncronas á vez. Imaxinemos este escenario: faise unha solicitude á base de datos de usuarios de Arfat, cómpre ler o seu campo profile_img_url e descargar unha imaxe do servidor someserver.com.
Despois da descarga, convertemos a imaxe a outro formato, por exemplo de PNG a JPEG. Se a conversión foi exitosa, envíase unha carta ao correo electrónico do usuario. A continuación, introdúcese a información sobre o evento no ficheiro transformations.log, indicando a data.

Paga a pena prestar atención á superposición de devolucións de chamada e á gran cantidade de }) na parte final do código. Chámase Callback Hell ou Pyramid of Doom.

As desvantaxes deste método son obvias:

  • Este código é difícil de ler.
  • Tamén é difícil xestionar os erros, o que moitas veces leva a unha mala calidade do código.

Para resolver este problema, engadíronse promesas a JavaScript. Permítenche substituír o aniñamento profundo das devolucións de chamada pola palabra .then.

O aspecto positivo das promesas é que fan que o código sexa moito mellor lexible, de arriba abaixo e non de esquerda a dereita. Non obstante, as promesas tamén teñen os seus problemas:

  • Cómpre engadir moitos .entón.
  • No canto de try/catch, úsase .catch para xestionar todos os erros.
  • Traballar con varias promesas nun bucle non sempre é conveniente; nalgúns casos, complican o código.

Aquí tes un problema que mostrará o significado do último punto.

Supoñamos que temos un bucle for que imprime unha secuencia de números de 0 a 10 a intervalos aleatorios (0–n segundos). Usando promesas, cómpre cambiar este bucle para que os números se impriman en secuencia de 0 a 10. Entón, se tarda 6 segundos en imprimir un cero e 2 segundos en imprimir un, primeiro debe imprimirse o cero e despois comezará a conta atrás para imprimir o.

E, por suposto, non usamos Async/Await ou .sort para resolver este problema. Un exemplo de solución está ao final.

Funcións asíncronas

A adición de funcións asíncronas en ES2017 (ES8) simplificou a tarefa de traballar con promesas. Observo que as funcións asíncronas funcionan "por enriba" das promesas. Estas funcións non representan conceptos cualitativamente diferentes. As funcións asíncronas están pensadas como unha alternativa ao código que usa promesas.

Async/Await permite organizar o traballo con código asíncrono nun estilo sincrónico.

Así, coñecer as promesas facilita a comprensión dos principios de Async/Await.

sintaxe

Normalmente, consta de dúas palabras clave: async e await. A primeira palabra converte a función en asíncrona. Tales funcións permiten o uso de await. En calquera outro caso, o uso desta función xerará un 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 insírese ao principio da declaración da función e, no caso dunha función de frecha, entre o signo "=" e os parénteses.

Estas funcións pódense colocar nun obxecto como métodos ou usar nunha declaración de clase.

// 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! Paga a pena lembrar que os construtores de clases e os getters/setters non poden ser asíncronos.

Semántica e regras de execución

As funcións asíncronas son basicamente similares ás funcións JS estándar, pero hai excepcións.

Así, as funcións asíncronas sempre devolven promesas:

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

En concreto, fn devolve a cadea ola. Ben, xa que esta é unha función asíncrona, o valor da cadea está envolto nunha promesa mediante un construtor.

Aquí tes un deseño alternativo sen Async:

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

Neste caso, a promesa devólvese "manualmente". Unha función asíncrona sempre está envolta nunha nova promesa.

Se o valor de retorno é un primitivo, a función asíncrona devolve o valor envolvéndoo nunha promesa. Se o valor de retorno é un obxecto de promesa, a súa resolución devólvese nunha nova promesa.

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

Pero que ocorre se hai un erro dentro dunha función asíncrona?

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

Se non se procesa, foo() devolverá unha promesa con rexeitamento. Nesta situación, devolverase Promise.reject que contén un erro en lugar de Promise.resolve.

As funcións asíncronas sempre emiten unha promesa, independentemente do que se devolva.

As funcións asíncronas fan unha pausa en cada espera.

Await afecta expresións. Entón, se a expresión é unha promesa, a función asíncrona suspende ata que se cumpra a promesa. Se a expresión non é unha promesa, convértese nunha promesa mediante Promise.resolve e despois complétase.

// 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 aquí tes unha descrición de como funciona a función fn.

  • Despois de chamalo, a primeira liña convértese de const a = await 9; en const a = agardar Promesa.resolver(9);.
  • Despois de usar Await, a execución da función está suspendida ata que a obteña o seu valor (na situación actual é 9).
  • delayAndGetRandom(1000) detén a execución da función fn ata que se complete (despois de 1 segundo). Isto detén efectivamente a función fn durante 1 segundo.
  • delayAndGetRandom(1000) mediante resolve devolve un valor aleatorio, que despois se asigna á variable b.
  • Ben, o caso da variable c é semellante ao caso da variable a. Despois diso, todo se detén por un segundo, pero agora delayAndGetRandom(1000) non devolve nada porque non é necesario.
  • Como resultado, os valores calcúlanse mediante a fórmula a + b * c. O resultado envólvese nunha promesa usando Promise.resolve e devólvese pola función.

Estas pausas poden lembrar aos xeradores en ES6, pero hai algo os teus motivos.

Resolvendo o problema

Ben, agora vexamos a solución ao problema mencionado anteriormente.

A función finishMyTask usa Await para esperar os resultados de operacións como queryDatabase, sendEmail, logTaskInFile e outras. Se comparas esta solución coa que se utilizaron as promesas, as semellanzas serán obvias. Non obstante, a versión Async/Await simplifica enormemente todas as complexidades sintácticas. Neste caso, non hai un gran número de devolucións de chamada e cadeas como .then/.catch.

Aquí tes unha solución coa saída de números, hai dúas opcións.

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 aquí tes unha solución usando funcións asíncronas.

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

Erro ao procesar

Os erros non controlados están envoltos nunha promesa rexeitada. Non obstante, as funcións asíncronas poden usar try/catch para xestionar erros de forma sincrónica.

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() é unha función asincrónica que se realiza correctamente ("número perfecto") ou falla cun erro ("Sentímolo, o número é demasiado grande").

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

Dado que o exemplo anterior espera que canRejectOrReturn se execute, o seu propio fallo producirá a execución do bloque catch. Como resultado, a función foo rematará con non definido (cando non se devolve nada no bloque try) ou cun erro detectado. Como resultado, esta función non fallará porque o try/catch xestionará a propia función foo.

Aquí tes outro exemplo:

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

Paga a pena prestar atención ao feito de que no exemplo, canRejectOrReturn é devolto de foo. Foo neste caso remata cun número perfecto ou devolve un erro ("Sentímolo, o número é demasiado grande"). O bloque catch nunca se executará.

O problema é que foo devolve a promesa pasada de canRejectOrReturn. Así, a solución para foo convértese na solución para canRejectOrReturn. Neste caso, o código constará só de dúas liñas:

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

Isto é o que ocorre se usas await e volves xuntos:

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

No código anterior, foo sairá correctamente con un número perfecto e un erro detectado. Aquí non haberá negativas. Pero foo volverá con canRejectOrReturn, non con undefined. Asegurémonos diso eliminando a liña de retorno await canRejectOrReturn():

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

Erros e trampas comúns

Nalgúns casos, o uso de Async/Await pode provocar erros.

Agarda esquecido

Isto ocorre con bastante frecuencia: esquécese a palabra clave await antes da promesa:

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

Como podes ver, non hai espera nin retorno no código. Polo tanto, foo sempre sae con undefined sen atraso de 1 segundo. Pero a promesa cumprirase. Se arroxa un erro ou rexeitamento, chamarase a UnhandledPromiseRejectionWarning.

Funcións asíncronas nas devolucións de chamada

As funcións asíncronas úsanse con bastante frecuencia en .map ou .filter como devolucións de chamada. Un exemplo é a función fetchPublicReposCount(nome de usuario), que devolve o número de repositorios abertos en GitHub. Digamos que hai tres usuarios cuxas métricas necesitamos. Aquí 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'];
}

Necesitamos contas ArfatSalman, octocat, norvig. Neste caso facemos:

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

Paga a pena prestar atención a Await na devolución de chamada .map. Aquí contas hai unha serie de promesas e .map é unha devolución de chamada anónima para cada usuario especificado.

Uso excesivamente consistente de await

Imos tomar 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;
}

Aquí o número de repositorio colócase na variable count, despois este número engádese á matriz counts. O problema do código é que ata que non cheguen os datos do primeiro usuario do servidor, todos os usuarios posteriores estarán en modo de espera. Así, só se procesa un usuario á vez.

Se, por exemplo, tarda uns 300 ms en procesar un usuario, entón para todos os usuarios xa é un segundo; o tempo empregado depende linealmente do número de usuarios. Pero como a obtención do número de repo non depende uns dos outros, os procesos pódense paralelizar. Isto require traballar con .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 recibe unha serie de promesas como entrada e devolve unha promesa. Este último, despois de que todas as promesas da matriz se completasen ou no primeiro rexeitamento, complétase. Pode ocorrer que non se inicien todos ao mesmo tempo; para garantir o inicio simultáneo, pode usar p-map.

Conclusión

As funcións asíncronas son cada vez máis importantes para o desenvolvemento. Ben, para o uso adaptativo das funcións asíncronas, deberías usar Iteradores asíncronos. Un desenvolvedor de JavaScript debería estar ben versado nisto.

Skillbox recomenda:

Fonte: www.habr.com

Engadir un comentario