Мысалдар арқылы JavaScript тіліндегі Async/Await қызметін қарастырайық

Мақала авторы JavaScript тіліндегі Async/Await мысалдарын қарастырады. Жалпы, Async/Await — асинхронды кодты жазудың ыңғайлы жолы. Бұл мүмкіндік пайда болғанға дейін мұндай код кері қоңыраулар мен уәделер арқылы жазылған. Бастапқы мақаланың авторы әртүрлі мысалдарды талдау арқылы Async/Await артықшылықтарын ашады.

Біз еске саламыз: «Хабрдың» барлық оқырмандары үшін - «Habr» жарнамалық кодын пайдаланып кез келген Skillbox курсына жазылу кезінде 10 000 рубль көлемінде жеңілдік.

Skillbox ұсынады: Білім беру онлайн курсы «Java әзірлеушісі».

повторного шақырудың

Кері қоңырау шалу белгісіз уақытқа кешіктірілетін функция. Бұрын кері қоңыраулар нәтижені бірден алу мүмкін емес код аймақтарында қолданылған.

Мұнда Node.js ішіндегі файлды асинхронды оқудың мысалы берілген:

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

Бір уақытта бірнеше асинхронды операцияларды орындау қажет болғанда проблемалар туындайды. Мына сценарийді елестетіп көрейік: Arfat пайдаланушы дерекқорына сұраныс жасалды, оның profile_img_url өрісін оқып, someserver.com серверінен суретті жүктеп алу керек.
Жүктеп алғаннан кейін суретті басқа форматқа түрлендіреміз, мысалы, PNG форматынан JPEG форматына. Егер түрлендіру сәтті болса, пайдаланушының электрондық поштасына хат жіберіледі. Содан кейін оқиға туралы ақпарат күнді көрсете отырып transformations.log файлына енгізіледі.

Кодтың соңғы бөлігіндегі кері қоңыраулардың қабаттасуына және }) көп санына назар аударған жөн. Оны кері шақыру тозағы немесе қиянат пирамидасы деп атайды.

Бұл әдістің кемшіліктері анық:

  • Бұл кодты оқу қиын.
  • Сондай-ақ қателерді өңдеу қиын, бұл көбінесе код сапасының нашарлауына әкеледі.

Бұл мәселені шешу үшін JavaScript-ке уәделер қосылды. Олар кері қоңыраулардың терең ұясын .then сөзімен ауыстыруға мүмкіндік береді.

Уәделердің жағымды жағы - олар кодты солдан оңға емес, жоғарыдан төмен қарай оқуға ыңғайлы етеді. Дегенмен, уәделердің де өз проблемалары бар:

  • Сізге көп .then қосу керек.
  • Барлық қателерді өңдеу үшін try/catch орнына .catch пайдаланылады.
  • Бір цикл ішінде бірнеше уәделермен жұмыс істеу әрқашан ыңғайлы емес, кейбір жағдайларда олар кодты қиындатады.

Міне, соңғы нүктенің мағынасын көрсететін есеп.

Кездейсоқ интервалдармен (0–n секунд) 10-ден 0-ға дейінгі сандар тізбегін басып шығаратын for циклі бар делік. Уәделерді пайдалана отырып, сандар 0-ден 10-ға дейін ретімен басып шығарылатындай етіп бұл циклды өзгерту керек. Демек, нөлді басып шығару үшін 6 секунд және бірді басып шығару үшін 2 секунд қажет болса, алдымен нөлді басып шығару керек, содан кейін басып шығару үшін кері санақ басталады.

Және, әрине, біз бұл мәселені шешу үшін Async/Await немесе .sort қолданбаймыз. Мысал шешім соңында.

Асинхронды функциялар

ES2017 (ES8) ішінде асинхронды функцияларды қосу уәделермен жұмыс істеу тапсырмасын жеңілдетеді. Мен асинхронды функциялардың уәделердің «жоғарғысында» жұмыс істейтінін ескеремін. Бұл функциялар әртүрлі сапалық ұғымдарды білдірмейді. Асинхронды функциялар уәделерді пайдаланатын кодқа балама ретінде арналған.

Async/Await синхронды стильде асинхронды кодпен жұмысты ұйымдастыруға мүмкіндік береді.

Осылайша, уәделерді білу Async/Await принциптерін түсінуді жеңілдетеді.

синтаксис

Әдетте ол екі кілт сөзден тұрады: асинхронды және күту. Бірінші сөз функцияны асинхрондыға айналдырады. Мұндай функциялар wait функциясын пайдалануға мүмкіндік береді. Кез келген басқа жағдайда бұл функцияны пайдалану қатені тудырады.

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

Асинхрондау функция декларациясының ең басында, ал көрсеткі функциясы жағдайында «=» белгісі мен жақшаның арасына енгізіледі.

Бұл функцияларды нысанда әдістер ретінде орналастыруға немесе сынып декларациясында пайдалануға болады.

// 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! Класс конструкторлары мен алушылардың/қондырғыштардың асинхронды бола алмайтынын есте ұстаған жөн.

Семантика және орындалу ережелері

Асинхронды функциялар негізінен стандартты JS функцияларына ұқсас, бірақ ерекше жағдайлар бар.

Осылайша, асинхронды функциялар әрқашан уәделерді қайтарады:

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

Атап айтқанда, fn hello жолын қайтарады. Бұл асинхронды функция болғандықтан, жолдың мәні конструкторды пайдаланып уәдеге оралады.

Мұнда Async жоқ балама дизайн бар:

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

Бұл жағдайда уәде «қолмен» қайтарылады. Асинхронды функция әрқашан жаңа уәдеге оралады.

Қайтару мәні қарабайыр болса, асинхронды функция оны уәдеге орау арқылы мәнді қайтарады. Егер қайтару мәні уәде нысаны болса, оның ажыратымдылығы жаңа уәдеде қайтарылады.

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

Бірақ асинхронды функцияның ішінде қате болса не болады?

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

Егер ол өңделмесе, foo() уәдені қабылдамау арқылы қайтарады. Бұл жағдайда Promise.resolve орнына қатесі бар Promise.reject қайтарылады.

Асинхронды функциялар қайтарылған нәрсеге қарамастан әрқашан уәдені шығарады.

Асинхронды функциялар әр күтуде үзіліс жасайды.

Күту өрнектерге әсер етеді. Сонымен, егер өрнек уәде болса, асинхронды функция уәде орындалғанға дейін тоқтатылады. Егер өрнек уәде болмаса, ол Promise.resolve арқылы уәдеге түрлендіріліп, содан кейін аяқталады.

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

Мұнда fn функциясының жұмысының сипаттамасы берілген.

  • Оны шақырғаннан кейін бірінші жол const a = await 9 түрленеді; const a = күту Promise.resolve(9);.
  • Await пайдаланғаннан кейін функцияның орындалуы a мәнін алғанша тоқтатылады (ағымдағы жағдайда ол 9).
  • delayAndGetRandom(1000) fn функциясының орындалуын өзі аяқталғанша (1 секундтан кейін) тоқтатады. Бұл fn функциясын 1 секундқа тиімді түрде тоқтатады.
  • delayAndGetRandom(1000) шешу арқылы кездейсоқ мәнді қайтарады, ол кейін b айнымалысына тағайындалады.
  • С айнымалысы бар жағдай а айнымалысы бар жағдайға ұқсас. Осыдан кейін бәрі бір секундқа тоқтайды, бірақ енді delayAndGetRandom(1000) ештеңені қайтармайды, себебі бұл қажет емес.
  • Нәтижесінде мәндер a + b * c формуласы арқылы есептеледі. Нәтиже Promise.resolve арқылы уәдеге оралады және функция арқылы қайтарылады.

Бұл үзілістер ES6-дағы генераторларды еске түсіруі мүмкін, бірақ оған бір нәрсе бар сіздің себептеріңіз.

Мәселені шешу

Ал, енді жоғарыда айтылған мәселенің шешімін қарастырайық.

finishMyTask функциясы queryDatabase, sendEmail, logTaskInFile және т.б. сияқты әрекеттердің нәтижелерін күту үшін Await функциясын пайдаланады. Егер сіз бұл шешімді уәделер қолданылған шешіммен салыстырсаңыз, ұқсастықтар айқын болады. Дегенмен, Async/Await нұсқасы барлық синтаксистік күрделіліктерді айтарлықтай жеңілдетеді. Бұл жағдайда .then/.catch сияқты кері шақырулар мен тізбектердің көп саны болмайды.

Міне, сандардың шығуы бар шешім, екі нұсқа бар.

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

Міне, асинхронды функцияларды қолданатын шешім.

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

Өңдеу қатесі

Өңделмеген қателер қабылданбаған уәдеге оралады. Дегенмен, синхронды функциялар қателерді синхронды өңдеу үшін try/catch функциясын пайдалана алады.

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() – асинхронды функция, ол сәтті орындалады («мінсіз сан») немесе қатемен («Кешіріңіз, сан тым үлкен»).

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

Жоғарыдағы мысал canRejectOrReturn орындалуын күтетіндіктен, оның өз сәтсіздігі catch блогының орындалуына әкеледі. Нәтижесінде, foo функциясы анықталмағанмен (try блогында ештеңе қайтарылмаған кезде) немесе қате табылумен аяқталады. Нәтижесінде бұл функция орындалмайды, себебі try/catch функциясы foo функциясын өзі өңдейді.

Міне, тағы бір мысал:

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

Мысалдағы canRejectOrReturn мәні foo-дан қайтарылғанына назар аударған жөн. Foo бұл жағдайда мінсіз санмен аяқталады немесе Қатені қайтарады («Кешіріңіз, сан тым үлкен»). Catch блогы ешқашан орындалмайды.

Мәселе мынада, foo canRejectOrReturn-дан берілген уәдені қайтарады. Сондықтан foo шешімі canRejectOrReturn шешімі болады. Бұл жағдайда код тек екі жолдан тұрады:

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

Wait және return функциясын бірге пайдалансаңыз не болады:

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

Жоғарыдағы кодта foo тамаша санмен де, ұсталған қатемен де сәтті шығады. Мұнда бас тартулар болмайды. Бірақ foo анықталмағанмен емес, canRejectOrReturn көмегімен қайтарылады. Қайтаруды күту canRejectOrReturn() жолын жою арқылы бұған көз жеткізейік:

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

Жалпы қателер мен қателіктер

Кейбір жағдайларда Async/Await пайдалану қателерге әкелуі мүмкін.

Ұмытылған күту

Бұл өте жиі орын алады - күту кілт сөзі уәдеден бұрын ұмытылады:

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

Көріп отырғаныңыздай, кодта күту немесе қайтару жоқ. Сондықтан foo әрқашан 1 секундтық кідіріссіз анықталмағанмен шығады. Бірақ уәде орындалады. Егер ол қатені немесе бас тартуды шығарса, UnhandledPromiseRejectionWarning шақырылады.

Кері қоңыраулардағы синхронды функциялар

Асинхронды функциялар .map немесе .filter ішінде кері шақырулар ретінде жиі пайдаланылады. Мысал GitHub жүйесіндегі ашық репозитарийлердің санын қайтаратын fetchPublicReposCount(username) функциясы болып табылады. Көрсеткіштері бізге қажет үш пайдаланушы бар делік. Міне, осы тапсырманың коды:

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

Бізге АрфатСалман, октокат, норвиг аккаунттары керек. Бұл жағдайда біз:

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

.map кері қоңыраудағы Күтуге назар аударған жөн. Мұндағы есептер - уәделер жиыны, ал .map - әрбір көрсетілген пайдаланушы үшін анонимді кері қоңырау.

Wait функциясын шамадан тыс тұрақты пайдалану

Мысал ретінде осы кодты алайық:

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

Мұнда репо нөмірі санау айнымалысына орналастырылады, содан кейін бұл сан counts массивіне қосылады. Кодқа қатысты мәселе бірінші пайдаланушының деректері серверден келгенше, барлық кейінгі пайдаланушылар күту режимінде болады. Осылайша, бір уақытта тек бір пайдаланушы өңделеді.

Егер, мысалы, бір пайдаланушыны өңдеуге шамамен 300 мс қажет болса, онда барлық пайдаланушылар үшін бұл қазірдің өзінде секунд; жұмсалған уақыт желілік түрде пайдаланушылар санына байланысты. Бірақ репо санын алу бір-біріне тәуелді болмағандықтан, процестерді параллельдеуге болады. Бұл .map және 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 кіріс ретінде уәделер жиымын алады және уәдені қайтарады. Соңғысы, массивтегі барлық уәделер аяқталғаннан кейін немесе бірінші бас тарту кезінде аяқталады. Олардың барлығы бір уақытта басталмауы мүмкін - бір уақытта іске қосуды қамтамасыз ету үшін p-map қолданбасын пайдалануға болады.

қорытынды

Асинхронды функциялар даму үшін маңыздырақ болып барады. Асинхронды функцияларды бейімді пайдалану үшін оны қолданған жөн Асинхронды итераторлар. JavaScript әзірлеушісі мұны жақсы білуі керек.

Skillbox ұсынады:

Ақпарат көзі: www.habr.com

пікір қалдыру