Ni rigardu Async/Await en JavaScript uzante ekzemplojn

La verkinto de la artikolo ekzamenas ekzemplojn de Async/Await en JavaScript. Ĝenerale, Async/Await estas oportuna maniero skribi nesinkronan kodon. Antaŭ ol ĉi tiu funkcio aperis, tia kodo estis skribita uzante revokojn kaj promesojn. La aŭtoro de la originala artikolo malkaŝas la avantaĝojn de Async/Await analizante diversajn ekzemplojn.

Ni memorigas vin: por ĉiuj legantoj de "Habr" - rabato de 10 000 rubloj kiam oni enskribas en iu ajn Skillbox-kurso per la reklamkodo "Habr".

Skillbox rekomendas: Eduka interreta kurso "Java programisto".

callback

Revoko estas funkcio, kies voko estas prokrastita senfine. Antaŭe, revokoj estis uzitaj en tiuj areoj de kodo kie la rezulto ne povus esti akirita tuj.

Jen ekzemplo de nesinkrone legado de dosiero en Node.js:

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

Problemoj aperas kiam vi devas plenumi plurajn nesinkronajn operaciojn samtempe. Ni imagu ĉi tiun scenaron: peto estas farita al la datumbazo de uzantoj de Arfat, vi devas legi ĝian kampon profile_img_url kaj elŝuti bildon de la servilo someserver.com.
Post elŝuto, ni konvertas la bildon al alia formato, ekzemple de PNG al JPEG. Se la konvertiĝo sukcesis, letero estas sendita al la retpoŝto de la uzanto. Poste, informoj pri la evento estas enmetitaj en la transforms.log-dosieron, indikante la daton.

Indas atenti la interkovron de revokoj kaj la granda nombro da }) en la fina parto de la kodo. Ĝi nomiĝas Callback Hell aŭ Pyramid of Doom.

La malavantaĝoj de ĉi tiu metodo estas evidentaj:

  • Ĉi tiu kodo estas malfacile legebla.
  • Ankaŭ estas malfacile trakti erarojn, kiuj ofte kondukas al malbona kodkvalito.

Por solvi ĉi tiun problemon, promesoj estis aldonitaj al JavaScript. Ili permesas al vi anstataŭigi profundan nestumon de revokoj per la vorto .then.

La pozitiva aspekto de promesoj estas ke ili faras la kodon multe pli bone legebla, de supre malsupre prefere ol de maldekstre dekstren. Tamen, promesoj ankaŭ havas siajn problemojn:

  • Vi devas aldoni multajn .tiam.
  • Anstataŭ try/catch, .catch estas uzata por trakti ĉiujn erarojn.
  • Labori kun multoblaj promesoj ene de unu buklo ne ĉiam estas oportuna; en iuj kazoj, ili malfaciligas la kodon.

Jen problemo, kiu montros la signifon de la lasta punkto.

Supozu ke ni havas for-buklon kiu presas sekvencon de nombroj de 0 ĝis 10 je hazardaj intervaloj (0–n sekundoj). Uzante promesojn, vi devas ŝanĝi ĉi tiun buklon por ke la nombroj estu presitaj en sinsekvo de 0 ĝis 10. Do, se necesas 6 sekundoj por presi nulon kaj 2 sekundojn por presi unu, la nulo devas esti presita unue, kaj tiam komenciĝos la retronombrado por presado de tiu.

Kaj kompreneble ni ne uzas Async/Await aŭ .sort por solvi ĉi tiun problemon. Ekzempla solvo estas ĉe la fino.

Nesinkronaj funkcioj

La aldono de nesinkronaj funkcioj en ES2017 (ES8) simpligis la taskon labori kun promesoj. Mi rimarkas, ke nesinkronaj funkcioj funkcias "supere" al promesoj. Ĉi tiuj funkcioj ne reprezentas kvalite malsamajn konceptojn. Nesinkronaj funkcioj estas celitaj kiel alternativo al kodo kiu uzas promesojn.

Async/Await ebligas organizi laboron kun nesinkrona kodo en sinkrona stilo.

Tiel, koni promesojn faciligas kompreni la principojn de Async/Await.

sintakso

Normale ĝi konsistas el du ŝlosilvortoj: async kaj await. La unua vorto igas la funkcion nesinkrona. Tiaj funkcioj permesas la uzon de atendu. En ajna alia kazo, uzi ĉi tiun funkcion generos eraron.

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

Async estas enmetita ĉe la komenco mem de la funkciodeklaro, kaj en la kazo de sagofunkcio, inter la "=" signo kaj la krampoj.

Ĉi tiuj funkcioj povas esti metitaj en objekton kiel metodoj aŭ uzataj en klasdeklaro.

// 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! Indas memori, ke klaskonstruantoj kaj getters/setters ne povas esti nesinkronaj.

Semantiko kaj ekzekutreguloj

Nesinkronaj funkcioj esence similas al normaj JS-funkcioj, sed estas esceptoj.

Tiel, nesinkronaj funkcioj ĉiam resendas promesojn:

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

Specife, fn resendas la ĉenon saluton. Nu, ĉar ĉi tio estas nesinkrona funkcio, la ĉenvaloro estas envolvita en promeso uzante konstruilon.

Jen alternativa dezajno sen Async:

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

En ĉi tiu kazo, la promeso estas resendita "mane". Nesinkrona funkcio ĉiam estas envolvita en nova promeso.

Se la revena valoro estas primitiva, la nesinkrona funkcio redonas la valoron envolvante ĝin en promeso. Se la revenvaloro estas promesobjekto, ĝia rezolucio estas resendita en nova promeso.

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

Sed kio okazas se estas eraro ene de nesinkrona funkcio?

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

Se ĝi ne estas procesita, foo() resendos promeson kun malakcepto. En ĉi tiu situacio, Promise.reject enhavanta eraron estos resendita anstataŭ Promise.resolve.

Nesinkronaj funkcioj ĉiam eligas promeson, sendepende de tio, kio estas resendita.

Nesinkronaj funkcioj paŭzas dum ĉiu atendado.

Atendi influas esprimojn. Do, se la esprimo estas promeso, la nesinkrona funkcio estas suspendita ĝis la promeso estas plenumita. Se la esprimo ne estas promeso, ĝi estas konvertita al promeso per Promise.resolve kaj poste kompletigita.

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

Kaj jen priskribo de kiel funkcias la fn-funkcio.

  • Post vokado de ĝi, la unua linio estas konvertita de konst a = await 9; en konst a = atendi Promeson.solvi(9);.
  • Post uzi Await, la funkcio ekzekuto estas suspendita ĝis a akiras sian valoron (en la nuna situacio ĝi estas 9).
  • delayAndGetRandom(1000) paŭzas la ekzekuton de la fn-funkcio ĝis ĝi kompletigas sin (post 1 sekundo). Ĉi tio efike haltigas la fn-funkcion dum 1 sekundo.
  • delayAndGetRandom(1000) per resolve resendas hazardan valoron, kiu tiam estas asignita al la variablo b.
  • Nu, la kazo kun variablo c similas al la kazo kun variablo a. Post tio, ĉio haltas por sekundo, sed nun delayAndGetRandom(1000) resendas nenion ĉar ĝi ne estas postulata.
  • Kiel rezulto, la valoroj estas kalkulitaj per la formulo a + b * c. La rezulto estas envolvita en promeso uzante Promise.resolve kaj resendita de la funkcio.

Ĉi tiuj paŭzoj eble memoras generatorojn en ES6, sed estas io viaj kialoj.

Solvante la problemon

Nu, nun ni rigardu la solvon de la supre menciita problemo.

La funkcio finiMyTask uzas Atendi por atendi la rezultojn de operacioj kiel queryDatabase, sendEmail, logTaskInFile, kaj aliaj. Se vi komparas ĉi tiun solvon kun tiu, kie oni uzis promesojn, la similecoj evidentiĝos. Tamen, la Async/Await-versio multe simpligas ĉiujn sintaksajn kompleksaĵojn. En ĉi tiu kazo, ne ekzistas granda nombro da revokoj kaj ĉenoj kiel .then/.catch.

Jen solvo kun eligo de nombroj, estas du ebloj.

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

Kaj jen solvo uzanta nesinkronajn funkciojn.

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

Eraro prilaborado

Netraktitaj eraroj estas envolvitaj en malakceptita promeso. Tamen, nesinkronaj funkcioj povas uzi try/catch por trakti erarojn sinkrone.

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() estas nesinkrona funkcio, kiu aŭ sukcesas ("perfekta nombro") aŭ malsukcesas pro eraro ("Pardonu, nombro tro granda").

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

Ĉar la supra ekzemplo atendas ke canRejectOrReturn efektiviĝos, ĝia propra malsukceso rezultigos la ekzekuton de la catch-bloko. Kiel rezulto, la funkcio foo finiĝos kun aŭ nedifinita (kiam nenio estas resendita en la try-bloko) aŭ kun eraro kaptita. Kiel rezulto, ĉi tiu funkcio ne malsukcesos ĉar la try/catch pritraktos la funkcion foo mem.

Jen alia ekzemplo:

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

Indas atenti la fakton, ke en la ekzemplo, canRejectOrReturn estas resendita de foo. Foo ĉi-kaze aŭ finiĝas per perfekta nombro aŭ resendas Eraron ("Pardonu, nombro tro granda"). La catch-bloko neniam estos ekzekutita.

La problemo estas, ke foo resendas la promeson de canRejectOrReturn. Do la solvo al foo fariĝas la solvo al canRejectOrReturn. En ĉi tiu kazo, la kodo konsistos el nur du linioj:

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

Jen kio okazas se vi uzas atendu kaj reveni kune:

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

En la supra kodo, foo sukcese eliros kun kaj perfekta nombro kaj eraro kaptita. Ĉi tie ne estos rifuzoj. Sed foo revenos kun canRejectOrReturn, ne kun undefined. Ni certigu tion per forigo de la returna await canRejectOrReturn() linio:

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

Oftaj eraroj kaj kaptiloj

En iuj kazoj, uzi Async/Await povas konduki al eraroj.

Forgesita atendo

Ĉi tio okazas sufiĉe ofte - la atendu ŝlosilvorto estas forgesita antaŭ la promeso:

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

Kiel vi povas vidi, ne estas atendo aŭ reveno en la kodo. Tial foo ĉiam eliras kun nedifinita sen 1 sekunda prokrasto. Sed la promeso estos plenumita. Se ĝi ĵetas eraron aŭ malakcepton, tiam UnhandledPromiseRejectionWarning estos nomita.

Nesinkronaj Funkcioj en Revokoj

Nesinkronaj funkcioj estas sufiĉe ofte uzataj en .map aŭ .filter kiel revoki. Ekzemplo estas la funkcio fetchPublicReposCount(uzantnomo), kiu resendas la nombron da malfermitaj deponejoj sur GitHub. Ni diru, ke estas tri uzantoj, kies metrikojn ni bezonas. Jen la kodo por ĉi tiu tasko:

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

Ni bezonas kontojn de ArfatSalman, octocat, norvig. En ĉi tiu kazo ni faras:

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

Indas atenti Await en la .map-revoko. Ĉi tie kalkulas estas aro da promesoj, kaj .map estas anonima revoko por ĉiu specifita uzanto.

Tro konsekvenca uzo de atendu

Ni prenu ĉi tiun kodon kiel ekzemplon:

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

Ĉi tie la repo-nombro estas metita en la kalkulvariablon, tiam ĉi tiu nombro estas aldonita al la kalkultabelo. La problemo kun la kodo estas, ke ĝis la datumoj de la unua uzanto alvenos de la servilo, ĉiuj postaj uzantoj estos en standby-reĝimo. Tiel, nur unu uzanto estas prilaborita samtempe.

Se, ekzemple, necesas ĉirkaŭ 300 ms por procesi unu uzanton, tiam por ĉiuj uzantoj ĝi jam estas sekundo; la tempo pasigita linie dependas de la nombro da uzantoj. Sed ĉar akiri la nombron da repo ne dependas unu de la alia, la procezoj povas esti paraleligitaj. Ĉi tio postulas labori kun .map kaj 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 ricevas tabelon da promesoj kiel enigaĵon kaj resendas promeson. Ĉi-lasta, post kiam ĉiuj promesoj en la tabelo finiĝis aŭ ĉe la unua malakcepto, estas kompletigita. Povas okazi, ke ili ĉiuj ne komenciĝas samtempe - por certigi samtempan komencon, vi povas uzi p-map.

konkludo

Nesinkronaj funkcioj iĝas ĉiam pli gravaj por evoluo. Nu, por adapta uzo de nesinkronaj funkcioj indas uzi ĝin Nesinkronaj Iteratoroj. JavaScript-programisto devus esti bone sperta pri tio.

Skillbox rekomendas:

fonto: www.habr.com

Aldoni komenton