Vegem Async/Await a JavaScript amb exemples

L'autor de l'article examina exemples de Async/Await a JavaScript. En general, Async/Await és una manera convenient d'escriure codi asíncron. Abans que aparegués aquesta funció, aquest codi es va escriure mitjançant devolucions de trucada i promeses. L'autor de l'article original revela els avantatges d'Async/Await analitzant diversos exemples.

Recordem: per a tots els lectors de "Habr": un descompte de 10 rubles en inscriure's a qualsevol curs de Skillbox amb el codi promocional "Habr".

Skillbox recomana: Curs educatiu en línia "Desenvolupador Java".

Devolució de trucada

La devolució de trucada és una funció la trucada de la qual es retarda indefinidament. Anteriorment, les devolucions de trucada s'utilitzaven en aquelles àrees de codi on el resultat no es podia obtenir immediatament.

Aquí teniu un exemple de lectura asíncrona d'un fitxer a Node.js:

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

Els problemes sorgeixen quan necessiteu realitzar diverses operacions asíncrones alhora. Imaginem aquest escenari: es fa una sol·licitud a la base de dades d'usuaris d'Arfat, cal llegir el seu camp profile_img_url i descarregar una imatge del servidor someserver.com.
Després de descarregar, convertim la imatge a un altre format, per exemple de PNG a JPEG. Si la conversió ha tingut èxit, s'envia una carta al correu electrònic de l'usuari. A continuació, s'introdueix informació sobre l'esdeveniment al fitxer transformations.log, indicant la data.

Val la pena parar atenció a la superposició de trucades i el gran nombre de }) a la part final del codi. Es diu Callback Hell o Pyramid of Doom.

Els desavantatges d'aquest mètode són evidents:

  • Aquest codi és difícil de llegir.
  • També és difícil gestionar els errors, cosa que sovint condueix a una mala qualitat del codi.

Per resoldre aquest problema, es van afegir promeses a JavaScript. Us permeten substituir la nidificació profunda de les devolucions de trucada amb la paraula .then.

L'aspecte positiu de les promeses és que fan que el codi sigui molt millor llegible, de dalt a baix en lloc d'esquerra a dreta. Tanmateix, les promeses també tenen els seus problemes:

  • Necessites afegir un munt de .then.
  • En lloc de try/catch, s'utilitza .catch per gestionar tots els errors.
  • Treballar amb diverses promeses dins d'un bucle no sempre és convenient; en alguns casos, compliquen el codi.

Aquí hi ha un problema que mostrarà el significat de l'últim punt.

Suposem que tenim un bucle for que imprimeix una seqüència de nombres del 0 al 10 a intervals aleatoris (0–n segons). Utilitzant promeses, heu de canviar aquest bucle perquè els números s'imprimeixin en seqüència del 0 al 10. Per tant, si es triguen 6 segons a imprimir un zero i 2 segons a imprimir un, primer s'ha d'imprimir el zero i després començarà el compte enrere per imprimir el.

I, per descomptat, no fem servir Async/Await o .sort per resoldre aquest problema. Un exemple de solució és al final.

Funcions asíncrones

L'addició de funcions asíncrones a ES2017 (ES8) va simplificar la tasca de treballar amb promeses. Observo que les funcions asíncrones funcionen "a sobre" de les promeses. Aquestes funcions no representen conceptes qualitativament diferents. Les funcions asíncrones estan pensades com una alternativa al codi que utilitza promeses.

Async/Await permet organitzar el treball amb codi asíncron en un estil síncron.

Per tant, conèixer promeses facilita la comprensió dels principis de Async/Await.

sintaxi

Normalment consta de dues paraules clau: async i await. La primera paraula converteix la funció en asíncrona. Aquestes funcions permeten l'ús de wait. En qualsevol altre cas, utilitzar aquesta funció generarà un error.

// With function declaration
 
async function myFn() {
  // await ...
}
 
// With arrow function
 
const myFn = async () => {
  // await ...
}
 
function myFn() {
  // await fn(); (Syntax Error since no async)
}
 

Async s'insereix al principi de la declaració de la funció i, en el cas d'una funció de fletxa, entre el signe “=” i els parèntesis.

Aquestes funcions es poden col·locar en un objecte com a mètodes o utilitzar-se en una declaració 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! Val la pena recordar que els constructors de classes i els getters/setters no poden ser asíncrons.

Semàntica i regles d'execució

Les funcions asíncrones són bàsicament similars a les funcions JS estàndard, però hi ha excepcions.

Per tant, les funcions asíncrones sempre retornen promeses:

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

Concretament, fn retorna la cadena hello. Bé, com que es tracta d'una funció asíncrona, el valor de la cadena s'embolica en una promesa mitjançant un constructor.

Aquí teniu un disseny alternatiu sense Async:

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

En aquest cas, la promesa es retorna "manualment". Una funció asíncrona sempre s'embolica en una nova promesa.

Si el valor de retorn és primitiu, la funció asíncrona retorna el valor embolicant-lo en una promesa. Si el valor de retorn és un objecte de promesa, la seva resolució es retorna en una nova promesa.

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

Però, què passa si hi ha un error dins d'una funció asíncrona?

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

Si no es processa, foo() retornarà una promesa amb rebuig. En aquesta situació, es retornarà Promise.reject que conté un error en lloc de Promise.resolve.

Les funcions asíncrones sempre produeixen una promesa, independentment del que es torni.

Les funcions asíncrones s'aturen en cada espera.

Await afecta les expressions. Així, si l'expressió és una promesa, la funció asíncrona es suspèn fins que es compleixi la promesa. Si l'expressió no és una promesa, es converteix en una promesa mitjançant Promise.resolve i després es completa.

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

I aquí hi ha una descripció de com funciona la funció fn.

  • Després de cridar-lo, la primera línia es converteix de const a = await 9; en const a = await Promise.resolve(9);.
  • Després d'utilitzar Await, l'execució de la funció es suspèn fins que a obté el seu valor (en la situació actual és 9).
  • delayAndGetRandom(1000) posa en pausa l'execució de la funció fn fins que es completa (al cap d'1 segon). Això atura efectivament la funció fn durant 1 segon.
  • delayAndGetRandom(1000) mitjançant resolve retorna un valor aleatori, que després s'assigna a la variable b.
  • Bé, el cas de la variable c és similar al cas de la variable a. Després d'això, tot s'atura durant un segon, però ara delayAndGetRandom(1000) no retorna res perquè no és necessari.
  • Com a resultat, els valors es calculen mitjançant la fórmula a + b * c. El resultat s'embolica en una promesa utilitzant Promise.resolve i la funció retorna.

Aquestes pauses poden recordar els generadors a ES6, però hi ha alguna cosa les teves raons.

Solucionant el problema

Bé, ara mirem la solució al problema esmentat anteriorment.

La funció finishMyTask utilitza Await per esperar els resultats d'operacions com queryDatabase, sendEmail, logTaskInFile i altres. Si compareu aquesta solució amb la que es van utilitzar promeses, les similituds es faran evidents. Tanmateix, la versió Async/Await simplifica enormement totes les complexitats sintàctiques. En aquest cas, no hi ha un gran nombre de trucades i cadenes com .then/.catch.

Aquí teniu una solució amb la sortida de números, hi ha dues opcions.

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

I aquí hi ha una solució que utilitza funcions asíncrones.

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

Error en processar

Els errors no gestionats s'emboliquen en una promesa rebutjada. Tanmateix, les funcions asíncrones poden utilitzar try/catch per gestionar els errors de manera 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() és una funció asíncrona que té èxit ("número perfecte") o falla amb un error ("Ho sento, el nombre és massa gran").

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

Com que l'exemple anterior espera que canRejectOrReturn s'executi, el seu propi error donarà lloc a l'execució del bloc catch. Com a resultat, la funció foo acabarà amb sense definir (quan no es retorna res al bloc try) o amb un error detectat. Com a resultat, aquesta funció no fallarà perquè try/catch gestionarà la pròpia funció foo.

Aquí teniu un altre exemple:

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

Val la pena parar atenció al fet que a l'exemple, canRejectOrReturn es retorna de foo. Foo en aquest cas acaba amb un nombre perfecte o retorna un error ("Ho sento, el número és massa gran"). El bloc catch no s'executarà mai.

El problema és que foo torna la promesa passada de canRejectOrReturn. Així que la solució a foo es converteix en la solució a canRejectOrReturn. En aquest cas, el codi constarà només de dues línies:

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

Això és el que passa si feu servir wait and return junts:

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

Al codi anterior, foo sortirà correctament amb un número perfecte i un error detectat. Aquí no hi haurà rebuigs. Però foo tornarà amb canRejectOrReturn, no amb undefined. Assegurem-nos d'això eliminant la línia de retorn await canRejectOrReturn():

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

Errors i trampes habituals

En alguns casos, utilitzar Async/Await pot provocar errors.

Espera oblidada

Això passa molt sovint: la paraula clau await s'oblida abans de la promesa:

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

Com podeu veure, no hi ha espera ni retorn al codi. Per tant, foo sempre surt amb undefined sense un retard d'1 segon. Però la promesa es complirà. Si genera un error o un rebuig, es cridarà a UnhandledPromiseRejectionWarning.

Funcions asíncrones a les devolució de trucades

Les funcions asíncrones s'utilitzen amb força freqüència a .map o .filter com a devolució de trucada. Un exemple és la funció fetchPublicReposCount(nom d'usuari), que retorna el nombre de repositoris oberts a GitHub. Suposem que hi ha tres usuaris les mètriques dels quals necessitem. Aquí teniu el codi per a aquesta tasca:

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

Necessitem comptes ArfatSalman, octocat, norvig. En aquest cas fem:

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

Val la pena parar atenció a Await a la devolució de trucada .map. Aquí compta amb una sèrie de promeses i .map és una devolució de trucada anònima per a cada usuari especificat.

Ús massa coherent de wait

Prenguem aquest codi com a exemple:

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í el número de repo es col·loca a la variable de recompte, després aquest número s'afegeix a la matriu de recomptes. El problema del codi és que fins que no arribin les dades del primer usuari del servidor, tots els usuaris posteriors estaran en mode d'espera. Així, només es processa un usuari alhora.

Si, per exemple, es triguen uns 300 ms a processar un usuari, per a tots els usuaris ja és un segon; el temps dedicat depèn linealment del nombre d'usuaris. Però com que l'obtenció del nombre de repo no depèn l'un de l'altre, els processos es poden paral·lelitzar. Això requereix treballar amb .map i 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 rep una sèrie de promeses com a entrada i retorna una promesa. Aquest últim, després que totes les promeses de la matriu s'hagin completat o al primer rebuig, es completa. Pot passar que no s'iniciïn tots al mateix temps; per tal de garantir l'inici simultània, podeu utilitzar p-map.

Conclusió

Les funcions asíncrones són cada cop més importants per al desenvolupament. Bé, per a l'ús adaptatiu de les funcions asíncrones, hauríeu d'utilitzar Iteradors asíncrons. Un desenvolupador de JavaScript hauria d'estar ben versat en això.

Skillbox recomana:

Font: www.habr.com

Afegeix comentari