Жишээ ашиглан JavaScript дээр Async/Await-ийг харцгаая

Өгүүллийн зохиогч JavaScript дахь Async/Await-ийн жишээнүүдийг судалсан. Ерөнхийдөө Async/Await нь асинхрон код бичихэд тохиромжтой арга юм. Энэ функц гарч ирэхээс өмнө ийм кодыг буцааж залгах болон амлалтуудыг ашиглан бичсэн. Анхны нийтлэлийн зохиогч янз бүрийн жишээн дээр дүн шинжилгээ хийх замаар Async/Await-ийн давуу талыг илчилсэн.

Бид танд сануулж байна: "Хабр" -ын бүх уншигчдад - "Habr" сурталчилгааны кодыг ашиглан Skillbox-ын аль ч курст бүртгүүлэхдээ 10 рублийн хөнгөлөлт.

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 гэдэг үгээр солих боломжийг олгодог.

Амлалтуудын эерэг тал нь кодыг зүүнээс баруун тийш биш дээрээс доош нь уншихад илүү хялбар болгодог. Гэсэн хэдий ч амлалтууд нь бас асуудалтай байдаг:

  • Та .тэгвэл их хэмжээгээр нэмэх хэрэгтэй.
  • Бүх алдааг зохицуулахад try/catch-ын оронд .catch ашигладаг.
  • Нэг давталт дотор олон амлалттай ажиллах нь тийм ч тохиромжтой биш бөгөөд зарим тохиолдолд кодыг төвөгтэй болгодог.

Сүүлчийн цэгийн утгыг харуулах бодлого энд байна.

0-ээс 10 хүртэлх тооны дарааллыг санамсаргүй интервалаар (0–n секунд) хэвлэдэг 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! Анги үүсгэгч ба хүлээн авагч/заагч нь асинхрон байж болохгүй гэдгийг санах нь зүйтэй.

Семантик ба гүйцэтгэлийн дүрэм

Async функцууд нь үндсэндээ стандарт 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 = wait 9; in const a = хүлээж байна Promise.resolve(9);.
  • Await-г ашигласны дараа функцийн гүйцэтгэлийг a утгыг нь авах хүртэл түр зогсооно (одоогийн нөхцөлд 9 байна).
  • delayAndGetRandom(1000) нь fn функцийн гүйцэтгэлийг өөрөө дуусах хүртэл (1 секундын дараа) түр зогсоодог. Энэ нь fn функцийг 1 секундын турш үр дүнтэй зогсооно.
  • delayAndGetRandom(1000) дамжуулан шийдвэрлэх нь санамсаргүй утгыг буцааж, дараа нь b хувьсагчид онооно.
  • За, в хувьсагчтай тохиолдол нь a хувьсагчтай тохиолдолтой төстэй. Үүний дараа бүх зүйл нэг секунд зогсох боловч одоо 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;
}

Хэрэв та await болон буцах хоёрыг хамтад нь ашиглавал юу болох вэ:

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

Дээрх кодонд foo төгс тоо болон баригдсан алдаатай амжилттай гарах болно. Эндээс татгалзах зүйл байхгүй. Гэхдээ foo нь тодорхойгүй байдлаар биш, харин canRejectOrReturn-ээр буцах болно. буцаах await 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(хэрэглэгчийн нэр) функц юм. Бидэнд хэмжигдэхүүн хэрэгтэй гурван хэрэглэгч байна гэж бодъё. Энэ даалгаврын код энд байна:

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-н буцаан дуудлагын Await-д анхаарлаа хандуулах нь зүйтэй. Энд counts нь олон тооны амлалтууд бөгөөд .map нь заасан хэрэглэгч бүрийн нэргүй буцаан дуудлага юм.

await-ийн хэт тогтмол хэрэглээ

Энэ кодыг жишээ болгон авч үзье:

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

Энд репо дугаарыг тоолох хувьсагчид байрлуулж, дараа нь энэ тоог тоолох массив дээр нэмнэ. Кодын асуудал нь эхний хэрэглэгчийн өгөгдөл серверээс ирэх хүртэл дараагийн бүх хэрэглэгчид зогсолтын горимд байх болно. Тиймээс нэг удаад зөвхөн нэг хэрэглэгч боловсруулагдана.

Жишээлбэл, нэг хэрэглэгчийг боловсруулахад ойролцоогоор 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

сэтгэгдэл нэмэх