Nümunələrdən istifadə edərək JavaScript-də Async/Await-ə baxaq

Məqalənin müəllifi JavaScript-də Async/Await nümunələrini araşdırır. Ümumiyyətlə, Async/Await asinxron kod yazmaq üçün əlverişli bir yoldur. Bu xüsusiyyət ortaya çıxmazdan əvvəl belə kod geri çağırışlar və vədlərdən istifadə edərək yazılırdı. Orijinal məqalənin müəllifi müxtəlif nümunələri təhlil edərək Async/Await-in üstünlüklərini açıqlayır.

Xatırladırıq: "Habr" ın bütün oxucuları üçün - "Habr" promosyon kodundan istifadə edərək hər hansı bir Skillbox kursuna yazılarkən 10 000 rubl endirim.

Skillbox tövsiyə edir: Onlayn təhsil kursu "Java Developer".

Callback

Geri çağırış, zəngi qeyri-müəyyən müddətə gecikdirilən bir funksiyadır. Əvvəllər, nəticəni dərhal əldə etmək mümkün olmayan kod sahələrində geri çağırışlardan istifadə olunurdu.

Node.js-də faylın asinxron oxunmasına bir nümunə:

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

Problemlər bir anda bir neçə asinxron əməliyyat yerinə yetirmək lazım olduqda yaranır. Bu ssenarini təsəvvür edək: Arfat istifadəçi məlumat bazasına sorğu verilir, onun profile_img_url sahəsini oxumaq və someserver.com serverindən şəkil yükləmək lazımdır.
Yüklədikdən sonra şəkli başqa formata, məsələn PNG-dən JPEG-ə çeviririk. Dönüşüm uğurlu olarsa, istifadəçinin elektron poçtuna məktub göndərilir. Sonra, hadisə haqqında məlumat tarix göstərilməklə transformations.log faylına daxil edilir.

Kodun son hissəsində geri çağırışların üst-üstə düşməsinə və çoxlu sayda }) diqqət yetirməyə dəyər. Buna Callback Hell və ya Piramida of Doom deyilir.

Bu metodun mənfi cəhətləri açıqdır:

  • Bu kodu oxumaq çətindir.
  • Səhvləri idarə etmək də çətindir, bu da çox vaxt pis kod keyfiyyətinə səbəb olur.

Bu problemi həll etmək üçün JavaScript-ə vədlər əlavə edildi. Onlar sizə geri zənglərin dərin yuvasını .sonra sözü ilə əvəz etməyə imkan verir.

Vədlərin müsbət tərəfi ondan ibarətdir ki, onlar kodu soldan sağa deyil, yuxarıdan aşağıya doğru daha yaxşı oxunaqlı edir. Bununla belə, vədlərin də öz problemləri var:

  • Siz çoxlu .sonra əlavə etməlisiniz.
  • Bütün səhvləri idarə etmək üçün try/catch əvəzinə .catch istifadə olunur.
  • Bir dövrədə bir neçə vədlə işləmək həmişə rahat deyil, bəzi hallarda kodu çətinləşdirir.

Burada son nöqtənin mənasını göstərəcək bir problem var.

Tutaq ki, təsadüfi intervallarla (0-n saniyə) 10-dan 0-a qədər rəqəmlər ardıcıllığını çap edən bir for dövrəmiz var. Vədlərdən istifadə edərək, bu döngəni elə dəyişdirməlisiniz ki, nömrələr ardıcıllıqla 0-dan 10-a qədər çap olunsun. Beləliklə, sıfırı çap etmək üçün 6 saniyə və bir çap etmək üçün 2 saniyə lazımdırsa, əvvəlcə sıfır, sonra isə çap edilməlidir. birini çap etmək üçün geri sayım başlayacaq.

Və təbii ki, biz bu problemi həll etmək üçün Async/Await və ya .sort istifadə etmirik. Məsələnin həlli sonundadır.

Async funksiyaları

ES2017-də (ES8) asinxron funksiyaların əlavə edilməsi vədlərlə işləmək tapşırığını sadələşdirdi. Qeyd edirəm ki, asinxron funksiyalar vədlərin “üstündə” işləyir. Bu funksiyalar keyfiyyətcə fərqli anlayışları təmsil etmir. Async funksiyaları vədlərdən istifadə edən koda alternativ kimi nəzərdə tutulub.

Async/Await asinxron kodla işi sinxron üslubda təşkil etməyə imkan verir.

Beləliklə, vədləri bilmək Async/Await prinsiplərini başa düşməyi asanlaşdırır.

sintaksis

Normalda o, iki açar sözdən ibarətdir: async və wait. Birinci söz funksiyanı asinxron vəziyyətə gətirir. Bu cür funksiyalar gözləmədən istifadə etməyə imkan verir. İstənilən başqa halda, bu funksiyadan istifadə xəta yaradacaq.

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

Async funksiya bəyannaməsinin ən əvvəlində, ox funksiyası vəziyyətində isə “=” işarəsi ilə mötərizələrin arasına daxil edilir.

Bu funksiyalar metod kimi obyektdə yerləşdirilə və ya sinif bəyannaməsində istifadə edilə bilər.

// 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! Yadda saxlamaq lazımdır ki, sinif konstruktorları və alıcılar/setterlər asinxron ola bilməz.

Semantika və icra qaydaları

Async funksiyaları əsasən standart JS funksiyalarına bənzəyir, lakin istisnalar var.

Beləliklə, async funksiyaları həmişə vədləri qaytarır:

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

Xüsusilə, fn salam sətirini qaytarır. Yaxşı, bu asinxron funksiya olduğundan, sətir dəyəri konstruktordan istifadə edərək bir sözə bükülür.

Async olmadan alternativ dizayn:

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

Bu halda, vəd "əl ilə" qaytarılır. Asinxron funksiya həmişə yeni bir sözə bükülür.

Qayıdış dəyəri primitivdirsə, asinxronizasiya funksiyası onu vəddə bükərək dəyəri qaytarır. Qaytarma dəyəri vəd obyektidirsə, onun həlli yeni vəddə qaytarılır.

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

Bəs asinxron funksiya daxilində xəta olarsa nə olar?

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

Əgər emal olunmazsa, foo() imtina ilə vədi qaytaracaq. Bu vəziyyətdə, Promise.resolve əvəzinə xəta ehtiva edən Promise.reject qaytarılacaq.

Async funksiyaları nəyin qaytarılmasından asılı olmayaraq həmişə vəd verir.

Asinxron funksiyalar hər gözləmədə fasilə verir.

Gözləmə ifadələrə təsir edir. Beləliklə, ifadə vəddirsə, vəd yerinə yetirilənə qədər async funksiyası dayandırılır. İfadə söz deyilsə, Promise.resolve vasitəsilə sözə çevrilir və sonra tamamlanır.

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

Və burada fn funksiyasının necə işlədiyinin təsviri verilmişdir.

  • Onu çağırdıqdan sonra birinci sətir const a = await 9-dan çevrilir; const a = gözləyin Promise.resolve(9);.
  • Await istifadə etdikdən sonra funksiyanın icrası a dəyərini alana qədər dayandırılır (cari vəziyyətdə 9-dur).
  • delayAndGetRandom(1000) fn funksiyası özünü tamamlayana qədər (1 saniyədən sonra) icrasını dayandırır. Bu, fn funksiyasını 1 saniyə effektiv şəkildə dayandırır.
  • delayAndGetRandom(1000) həll yolu ilə təsadüfi qiymət qaytarır və bu daha sonra b dəyişəninə təyin edilir.
  • Yaxşı, c dəyişəni ilə vəziyyət a dəyişəni ilə oxşardır. Bundan sonra hər şey bir saniyə dayanır, lakin indi delayAndGetRandom(1000) tələb olunmadığı üçün heç nə qaytarmır.
  • Nəticədə, dəyərlər a + b * c düsturu ilə hesablanır. Nəticə Promise.resolve istifadə edərək vədlə bükülür və funksiya tərəfindən qaytarılır.

Bu fasilələr ES6-dakı generatorları xatırlada bilər, lakin bunda bir şey var Səbəbləriniz.

Problemin həlli

Yaxşı, indi yuxarıda qeyd olunan problemin həllinə baxaq.

finishMyTask funksiyası queryDatabase, sendEmail, logTaskInFile və başqaları kimi əməliyyatların nəticələrini gözləmək üçün Await funksiyasından istifadə edir. Bu həlli vədlərin istifadə edildiyi ilə müqayisə etsəniz, oxşarlıqlar aydın olacaq. Bununla belə, Async/Await versiyası bütün sintaktik mürəkkəblikləri xeyli asanlaşdırır. Bu halda, .then/.catch kimi çox sayda geri çağırış və zəncir yoxdur.

Burada nömrələrin çıxışı ilə bir həll var, iki variant var.

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

Və burada async funksiyalarından istifadə edən bir həll var.

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

Emal zamanı xəta

İşlənməmiş səhvlər rədd edilmiş vədlə əhatə olunur. Bununla belə, asinxron funksiyalar səhvləri sinxron şəkildə idarə etmək üçün cəhd/tutmaqdan istifadə edə bilər.

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() ya uğur qazanan (“mükəmməl nömrə”) və ya xəta ilə uğursuz (“Bağışlayın, nömrə çox böyük”) asinxron funksiyadır.

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

Yuxarıdakı nümunə canRejectOrReturn-un icrasını gözlədiyi üçün onun öz uğursuzluğu catch blokunun icrası ilə nəticələnəcək. Nəticədə, foo funksiyası ya qeyri-müəyyən (sınaq blokunda heç nə qaytarılmadıqda) və ya tutulma xətası ilə başa çatacaq. Nəticədə, bu funksiya uğursuz olmayacaq, çünki try/catch foo funksiyasını özü idarə edəcək.

Budur başqa bir nümunə:

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

Nümunədə canRejectOrReturn-un foo-dan qaytarıldığına diqqət yetirməyə dəyər. Foo bu halda ya mükəmməl nömrə ilə başa çatır, ya da Xəta qaytarır (“Bağışlayın, nömrə çox böyük”). Tutma bloku heç vaxt icra olunmayacaq.

Problem ondadır ki, foo canRejectOrReturn-dan verilən vədi qaytarır. Beləliklə, foo həlli canRejectOrReturn həllinə çevrilir. Bu halda kod yalnız iki sətirdən ibarət olacaq:

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

Birlikdə gözləmək və qayıtmaqdan istifadə etsəniz nə baş verir:

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

Yuxarıdakı kodda foo həm mükəmməl nömrə, həm də tutulan səhvlə uğurla çıxacaq. Burada heç bir imtina olmayacaq. Lakin foo, müəyyən edilməmiş deyil, canRejectOrReturn ilə qayıdacaq. Gəlin buna əmin olaq canRejectOrReturn() sətrinin qaytarılmasını gözləyir:

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

Ümumi səhvlər və tələlər

Bəzi hallarda Async/Await-dən istifadə xətalara səbəb ola bilər.

Unudulmuş gözləyir

Bu olduqca tez-tez olur - await açar sözü sözdən əvvəl unudulur:

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

Gördüyünüz kimi, kodda gözləmə və ya geri dönüş yoxdur. Buna görə də foo həmişə 1 saniyə gecikmə olmadan qeyri-müəyyənliklə çıxır. Amma vəd yerinə yetiriləcək. Səhv və ya rədd cavabı verirsə, UnhandledPromiseRejectionWarning çağırılacaq.

Geri Zənglərdə Async Funksiyaları

Async funksiyaları çox vaxt .map və ya .filterdə geri çağırış kimi istifadə olunur. Məsələn, GitHub-da açıq depoların sayını qaytaran fetchPublicReposCount(istifadəçi adı) funksiyasıdır. Deyək ki, üç istifadəçi var, onların ölçüləri bizə lazımdır. Bu tapşırığın kodu budur:

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

Bizə ArfatSalman, octocat, norvig hesabları lazımdır. Bu halda biz:

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

.map geri çağırışında Gözləməyə diqqət yetirməyə dəyər. Burada hesablar bir sıra vədlərdir və .map hər bir müəyyən istifadəçi üçün anonim geri çağırışdır.

Wait-in həddindən artıq ardıcıl istifadəsi

Nümunə olaraq bu kodu götürək:

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

Burada repo nömrəsi count dəyişəninə yerləşdirilir, sonra bu rəqəm counts massivinə əlavə olunur. Kodla bağlı problem ondadır ki, birinci istifadəçinin məlumatları serverdən gələnə qədər bütün sonrakı istifadəçilər gözləmə rejimində olacaqlar. Beləliklə, bir anda yalnız bir istifadəçi işlənir.

Məsələn, bir istifadəçini emal etmək təxminən 300 ms çəkirsə, bütün istifadəçilər üçün bu, artıq bir saniyədir; sərf olunan vaxt xətti olaraq istifadəçilərin sayından asılıdır. Amma repo sayının əldə edilməsi bir-birindən asılı olmadığı üçün prosesləri paralelləşdirmək olar. Bunun üçün .map və Promise.all ilə işləmək lazımdır:

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

Promise.all giriş kimi bir sıra vədlər alır və vədi qaytarır. Sonuncu, massivdəki bütün vədlər tamamlandıqdan və ya ilk imtinada tamamlanır. Onların hamısı eyni vaxtda başlamaması baş verə bilər - eyni vaxtda başlanğıcı təmin etmək üçün p-map-dan istifadə edə bilərsiniz.

Nəticə

Async funksiyaları inkişaf üçün getdikcə daha vacib olur. Yaxşı, async funksiyalarının adaptiv istifadəsi üçün istifadə etməlisiniz Async iterators. JavaScript tərtibatçısı bunu yaxşı bilməlidir.

Skillbox tövsiyə edir:

Mənbə: www.habr.com

Добавить комментарий