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.
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.
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.
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.
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:
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:
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:
.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:
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.