Gadewch i ni edrych ar Async/Await yn JavaScript gan ddefnyddio enghreifftiau

Mae awdur yr erthygl yn archwilio enghreifftiau o Async/Await yn JavaScript. Ar y cyfan, mae Async/Await yn ffordd gyfleus o ysgrifennu cod asyncronig. Cyn i'r nodwedd hon ymddangos, ysgrifennwyd cod o'r fath gan ddefnyddio galwadau yn ôl ac addewidion. Mae awdur yr erthygl wreiddiol yn datgelu manteision Async/Await trwy ddadansoddi enghreifftiau amrywiol.

Rydym yn atgoffa: i holl ddarllenwyr "Habr" - gostyngiad o 10 rubles wrth gofrestru ar unrhyw gwrs Skillbox gan ddefnyddio'r cod hyrwyddo "Habr".

Mae Skillsbox yn argymell: Cwrs addysgol ar-lein "datblygwr Java".

Galw'n ôl

Mae galwad yn ôl yn swyddogaeth y mae ei galwad yn cael ei gohirio am gyfnod amhenodol. Yn flaenorol, defnyddiwyd galwadau yn ôl yn y meysydd cod hynny lle na ellid cael y canlyniad ar unwaith.

Dyma enghraifft o ddarllen ffeil yn Node.js yn anghydamserol:

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

Mae problemau'n codi pan fydd angen i chi gyflawni sawl llawdriniaeth asyncronig ar unwaith. Gadewch i ni ddychmygu'r senario hwn: gwneir cais i gronfa ddata defnyddwyr Arfat, mae angen i chi ddarllen ei faes profile_img_url a lawrlwytho delwedd o'r gweinydd someserver.com.
Ar ôl ei lawrlwytho, rydym yn trosi'r ddelwedd i fformat arall, er enghraifft o PNG i JPEG. Os bu'r trosiad yn llwyddiannus, anfonir llythyr at e-bost y defnyddiwr. Nesaf, rhoddir gwybodaeth am y digwyddiad yn y ffeil Transforms.log, gan nodi'r dyddiad.

Mae'n werth rhoi sylw i'r gorgyffwrdd o alwadau'n ôl a'r nifer fawr o }) yn rhan olaf y cod. Fe'i gelwir yn Callback Hell neu Pyramid of Doom.

Mae anfanteision y dull hwn yn amlwg:

  • Mae'r cod hwn yn anodd ei ddarllen.
  • Mae hefyd yn anodd trin gwallau, sy'n aml yn arwain at ansawdd cod gwael.

I ddatrys y broblem hon, ychwanegwyd addewidion at JavaScript. Maent yn caniatáu ichi ddisodli nythu dwfn o alwadau'n ôl gyda'r gair .then.

Agwedd gadarnhaol addewidion yw eu bod yn gwneud y cod yn llawer gwell darllenadwy, o'r top i'r gwaelod yn hytrach nag o'r chwith i'r dde. Fodd bynnag, mae gan addewidion eu problemau hefyd:

  • Mae angen ychwanegu llawer o .yna.
  • Yn lle ceisio/dal, defnyddir .catch i drin pob gwall.
  • Nid yw gweithio gydag addewidion lluosog o fewn un ddolen bob amser yn gyfleus; mewn rhai achosion, maent yn cymhlethu'r cod.

Dyma broblem a fydd yn dangos ystyr y pwynt olaf.

Tybiwch fod gennym ddolen ar gyfer sy'n argraffu dilyniant o rifau o 0 i 10 ar hap (0–n eiliad). Gan ddefnyddio addewidion, mae angen i chi newid y ddolen hon fel bod y niferoedd yn cael eu hargraffu mewn dilyniant o 0 i 10. Felly, os yw'n cymryd 6 eiliad i argraffu sero a 2 eiliad i argraffu un, dylid argraffu'r sero yn gyntaf, ac yna bydd y cyfrif i lawr ar gyfer argraffu'r un yn dechrau.

Ac wrth gwrs, nid ydym yn defnyddio Async/Await neu .sort i ddatrys y broblem hon. Mae datrysiad enghreifftiol ar y diwedd.

Swyddogaethau async

Mae ychwanegu swyddogaethau async yn ES2017 (ES8) wedi symleiddio'r dasg o weithio gydag addewidion. Sylwaf fod swyddogaethau async yn gweithio “ar ben” addewidion. Nid yw'r swyddogaethau hyn yn cynrychioli cysyniadau ansoddol wahanol. Bwriedir swyddogaethau async fel dewis amgen i god sy'n defnyddio addewidion.

Mae Async/Await yn ei gwneud hi'n bosibl trefnu gwaith gyda chod asyncronig mewn arddull cydamserol.

Felly, mae gwybod addewidion yn ei gwneud hi'n haws deall egwyddorion Async/Await.

cystrawen

Fel arfer mae'n cynnwys dau allweddair: async ac aros. Mae'r gair cyntaf yn troi'r ffwythiant yn asyncronig. Mae swyddogaethau o'r fath yn caniatáu defnyddio aros. Mewn unrhyw achos arall, bydd defnyddio'r swyddogaeth hon yn creu gwall.

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

Mewnosodir async ar ddechrau'r datganiad swyddogaeth, ac yn achos ffwythiant saeth, rhwng yr arwydd “=” a'r cromfachau.

Gellir gosod y swyddogaethau hyn mewn gwrthrych fel dulliau neu eu defnyddio mewn datganiad dosbarth.

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

DS! Mae'n werth cofio na all llunwyr dosbarth a derbynwyr/gosodwyr fod yn anghydamserol.

Semanteg a rheolau gweithredu

Yn y bôn, mae swyddogaethau async yn debyg i swyddogaethau safonol JS, ond mae yna eithriadau.

Felly, mae swyddogaethau async bob amser yn dychwelyd addewidion:

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

Yn benodol, mae fn yn dychwelyd y llinyn helo. Wel, gan fod hon yn swyddogaeth asyncronig, mae gwerth y llinyn wedi'i lapio mewn addewid gan ddefnyddio adeiladwr.

Dyma ddyluniad amgen heb Async:

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

Yn yr achos hwn, dychwelir yr addewid “â llaw”. Mae swyddogaeth asynchronous bob amser wedi'i lapio mewn addewid newydd.

Os yw'r gwerth dychwelyd yn gyntefig, mae'r swyddogaeth async yn dychwelyd y gwerth trwy ei lapio mewn addewid. Os yw'r gwerth dychwelyd yn wrthrych addewid, dychwelir ei benderfyniad mewn addewid newydd.

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

Ond beth sy'n digwydd os oes gwall y tu mewn i swyddogaeth asyncronig?

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

Os na chaiff ei brosesu, bydd foo() yn dychwelyd addewid gyda gwrthodiad. Yn y sefyllfa hon, bydd Promise.reject yn cynnwys gwall yn cael ei ddychwelyd yn lle Promise.resolve.

Mae swyddogaethau Async bob amser yn allbynnu addewid, ni waeth beth sy'n cael ei ddychwelyd.

Mae swyddogaethau asyncronaidd yn aros bob amser.

Aros yn effeithio ar ymadroddion. Felly, os yw'r ymadrodd yn addewid, mae'r swyddogaeth async yn cael ei hatal nes cyflawni'r addewid. Os nad yw'r ymadrodd yn addewid, caiff ei drawsnewid yn addewid trwy Promise.resolve ac yna ei gwblhau.

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

A dyma ddisgrifiad o sut mae'r ffwythiant fn yn gweithio.

  • Ar ôl ei alw, mae'r llinell gyntaf yn cael ei throsi o const a = aros 9; yn const a = disgwyl Promise.resolve(9);.
  • Ar ôl defnyddio Await, mae gweithrediad y swyddogaeth yn cael ei atal nes bod a yn cael ei werth (yn y sefyllfa bresennol mae'n 9).
  • Mae delayAndGetRandom(1000) yn oedi gweithrediad y ffwythiant fn nes iddi gwblhau ei hun (ar ôl 1 eiliad). Mae hyn i bob pwrpas yn atal y swyddogaeth fn am 1 eiliad.
  • Mae delayAndGetRandom(1000) trwy resolution yn dychwelyd gwerth ar hap, sydd wedyn yn cael ei neilltuo i'r newidyn b.
  • Wel, mae'r achos gyda newidyn c yn debyg i'r achos gyda newidyn a. Ar ôl hynny, mae popeth yn stopio am eiliad, ond nawr nid yw oediAndGetRandom(1000) yn dychwelyd dim oherwydd nad oes ei angen.
  • O ganlyniad, mae'r gwerthoedd yn cael eu cyfrifo gan ddefnyddio'r fformiwla a + b * c. Mae'r canlyniad wedi'i lapio mewn addewid gan ddefnyddio Promise.resolve a'i ddychwelyd gan y swyddogaeth.

Efallai bod y seibiau hyn yn atgoffa rhywun o eneraduron yn ES6, ond mae rhywbeth yn ei gylch eich rhesymau.

Datrys y broblem

Wel, nawr gadewch i ni edrych ar yr ateb i'r broblem a grybwyllwyd uchod.

Mae swyddogaeth finishMyTask yn defnyddio Aros i aros am ganlyniadau gweithrediadau fel queryDatabase, sendEmail, logTaskInFile, ac eraill. Os cymharwch yr ateb hwn â'r un lle defnyddiwyd addewidion, bydd y tebygrwydd yn dod yn amlwg. Fodd bynnag, mae'r fersiwn Async/Await yn symleiddio'r holl gymhlethdodau cystrawen yn fawr. Yn yr achos hwn, nid oes nifer fawr o alwadau'n ôl a chadwyni fel .then/.catch.

Dyma ateb gydag allbwn rhifau, mae dau opsiwn.

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

A dyma ateb gan ddefnyddio swyddogaethau async.

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

Prosesu gwallau

Mae gwallau heb eu trin yn cael eu lapio mewn addewid a wrthodwyd. Fodd bynnag, gall swyddogaethau async ddefnyddio ceisio / dal i drin gwallau yn gydamserol.

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

Mae canRejectOrReturn() yn ffwythiant asyncronaidd sydd naill ai'n llwyddo (“rhif perffaith”) neu'n methu â gwall (“Sori, mae'r rhif yn rhy fawr”).

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

Gan fod yr enghraifft uchod yn disgwyl canRejectOrReturn i weithredu, bydd ei fethiant ei hun yn arwain at weithredu'r bloc dal. O ganlyniad, bydd y ffwythiant foo yn gorffen gyda naill ai heb ei ddiffinio (pan na fydd dim yn cael ei ddychwelyd yn y bloc ceisio) neu gyda gwall wedi'i ddal. O ganlyniad, ni fydd y swyddogaeth hon yn methu oherwydd bydd y cais / dal yn trin y ffwythiant ei hun.

Dyma enghraifft arall:

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

Mae'n werth talu sylw i'r ffaith bod canRejectOrReturn yn cael ei ddychwelyd o foo yn yr enghraifft. Mae Foo yn yr achos hwn naill ai'n terfynu gyda rhif perffaith neu'n dychwelyd Gwall (“Mae'n ddrwg gennyf, mae'r rhif yn rhy fawr”). Ni fydd y bloc dal byth yn cael ei weithredu.

Y broblem yw bod foo yn dychwelyd yr addewid a basiwyd o canRejectOrReturn. Felly yr ateb i foo yw'r ateb i canRejectOrReturn. Yn yr achos hwn, bydd y cod yn cynnwys dwy linell yn unig:

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

Dyma beth sy'n digwydd os ydych chi'n defnyddio aros a dychwelyd gyda'ch gilydd:

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

Yn y cod uchod, bydd foo yn gadael yn llwyddiannus gyda rhif perffaith a gwall wedi'i ddal. Ni fydd unrhyw wrthodiadau yma. Ond bydd foo yn dychwelyd gyda canRejectOrReturn, nid gyda heb ei ddiffinio. Gadewch i ni wneud yn siŵr o hyn trwy ddileu'r llinell dychwelyd aros canRejectOrReturn():

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

Camgymeriadau a pheryglon cyffredin

Mewn rhai achosion, gall defnyddio Async/Await arwain at wallau.

Wedi anghofio aros

Mae hyn yn digwydd yn eithaf aml - mae'r allweddair aros yn cael ei anghofio cyn yr addewid:

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

Fel y gwelwch, nid oes disgwyl na dychwelyd yn y cod. Felly mae foo bob amser yn gadael heb ei ddiffinio heb oedi o 1 eiliad. Ond bydd yr addewid yn cael ei chyflawni. Os yw'n taflu gwall neu wrthodiad, yna bydd UnhandledPromiseRejectionWarning yn cael ei alw.

Swyddogaethau Cydamseru mewn Galw'n Ôl

Defnyddir swyddogaethau async yn aml mewn .map neu .filter fel galwadau yn ôl. Enghraifft yw swyddogaeth fetchPublicReposCount(enw defnyddiwr), sy'n dychwelyd nifer y storfeydd agored ar GitHub. Gadewch i ni ddweud bod yna dri defnyddiwr y mae angen eu metrigau arnom. Dyma'r cod ar gyfer y dasg hon:

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

Mae arnom angen cyfrifon ArfatSalman, octocat, norvig. Yn yr achos hwn rydym yn gwneud:

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

Mae'n werth talu sylw i Aros yn y galwad yn ôl .map. Yma mae cyfrif yn amrywiaeth o addewidion, ac mae .map yn alwad yn ôl yn ddienw ar gyfer pob defnyddiwr penodedig.

Defnydd rhy gyson o aros

Gadewch i ni gymryd y cod hwn fel enghraifft:

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

Yma mae'r rhif repo yn cael ei roi yn y newidyn cyfrif, yna mae'r rhif hwn yn cael ei ychwanegu at yr arae cyfrif. Y broblem gyda'r cod yw hyd nes bod data'r defnyddiwr cyntaf yn cyrraedd o'r gweinydd, bydd yr holl ddefnyddwyr dilynol yn y modd segur. Felly, dim ond un defnyddiwr sy'n cael ei brosesu ar y tro.

Er enghraifft, os yw'n cymryd tua 300 ms i brosesu un defnyddiwr, yna i bob defnyddiwr mae eisoes yn eiliad; mae'r amser a dreulir yn llinol yn dibynnu ar nifer y defnyddwyr. Ond gan nad yw cael nifer y repo yn dibynnu ar ei gilydd, gellir cyfochrog â'r prosesau. Mae hyn yn gofyn am weithio gyda .map ac Promise.all:

async function fetchAllCounts(users) {
  const promises = users.map(async username => {
    const count = await fetchPublicReposCount(username);
    return count;
  });
  return Promise.all(promises);
}

Mae Promise.all yn derbyn amrywiaeth o addewidion fel mewnbwn ac yn dychwelyd addewid. Mae'r olaf, ar ôl i bob addewid yn yr arae gael ei gwblhau neu ar y gwrthodiad cyntaf, yn cael ei gwblhau. Gall ddigwydd nad ydynt i gyd yn dechrau ar yr un pryd - er mwyn sicrhau cychwyn ar yr un pryd, gallwch ddefnyddio p-map.

Casgliad

Mae swyddogaethau async yn dod yn fwyfwy pwysig ar gyfer datblygiad. Wel, ar gyfer defnydd addasol o swyddogaethau async, dylech ddefnyddio Iterators Async. Dylai datblygwr JavaScript fod yn hyddysg yn hyn.

Mae Skillsbox yn argymell:

Ffynhonnell: hab.com

Ychwanegu sylw