Örnekleri kullanarak JavaScript'teki Async/Await'e bakalım

Makalenin yazarı, JavaScript'teki Async/Await örneklerini inceliyor. Genel olarak Async/Await, asenkron kod yazmanın kullanışlı bir yoludur. Bu özellik ortaya çıkmadan önce bu tür kodlar geri aramalar ve vaatler kullanılarak yazılıyordu. Orijinal makalenin yazarı, çeşitli örnekleri analiz ederek Async/Await'in avantajlarını ortaya koyuyor.

Hatırlatıyoruz: tüm "Habr" okuyucuları için - "Habr" promosyon kodunu kullanarak herhangi bir Skillbox kursuna kayıt olurken 10 ruble indirim.

Skillbox şunları önerir: Eğitici çevrimiçi kurs "Java geliştirici".

Geri Arama

Geri arama, çağrısı süresiz olarak ertelenen bir işlevdir. Daha önce, sonucun hemen alınamadığı kod alanlarında geri aramalar kullanılıyordu.

Node.js'de bir dosyayı eşzamansız olarak okumaya bir örnek:

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

Aynı anda birden fazla eşzamansız işlemi gerçekleştirmeniz gerektiğinde sorunlar ortaya çıkar. Şu senaryoyu hayal edelim: Arfat kullanıcı veritabanına bir istek yapılıyor, profile_img_url alanını okumanız ve someserver.com sunucusundan bir resim indirmeniz gerekiyor.
İndirdikten sonra görüntüyü başka bir formata, örneğin PNG'den JPEG'e dönüştürüyoruz. Dönüşüm başarılı olursa kullanıcının e-postasına bir mektup gönderilir. Daha sonra, olayla ilgili bilgiler, tarihi belirterek transforms.log dosyasına girilir.

Kodun son kısmındaki geri aramaların örtüşmesine ve çok sayıda }) sayısına dikkat etmek önemlidir. Buna Geri Çağırma Cehennemi veya Kıyamet Piramidi denir.

Bu yöntemin dezavantajları açıktır:

  • Bu kodun okunması zordur.
  • Ayrıca hataların üstesinden gelmek de zordur, bu da çoğu zaman kötü kod kalitesine yol açar.

Bu sorunu çözmek için JavaScript'e sözler eklendi. Geri aramaların derin iç içe yerleştirilmesini .then sözcüğüyle değiştirmenize olanak tanır.

Vaatlerin olumlu yönü, kodu soldan sağa değil, yukarıdan aşağıya doğru çok daha iyi okunabilir hale getirmeleridir. Ancak vaatlerin de kendi sorunları var:

  • Çok fazla .then eklemeniz gerekir.
  • Tüm hataları işlemek için try/catch yerine .catch kullanılır.
  • Tek bir döngüde birden fazla vaatle çalışmak her zaman uygun değildir; bazı durumlarda kodu karmaşık hale getirir.

İşte son noktanın anlamını gösterecek bir problem.

Diyelim ki, rastgele aralıklarla (0-n saniye) 10'dan 0'a kadar bir sayı dizisi yazdıran bir for döngümüz var. Vaatleri kullanarak, bu döngüyü, sayıların 0'dan 10'a kadar sırayla yazdırılacağı şekilde değiştirmeniz gerekir. Yani, sıfır basmak 6 saniye ve bir basmak 2 saniye sürerse, önce sıfır basılmalı ve sonra basılmalıdır. birini basmak için geri sayım başlayacak.

Ve elbette bu sorunu çözmek için Async/Await veya .sort kullanmıyoruz. Örnek bir çözüm sondadır.

Eşzamansız işlevler

ES2017'ye (ES8) eşzamansız işlevlerin eklenmesi, vaatlerle çalışma görevini basitleştirdi. Eşzamansız işlevlerin vaatlerin "üzerinde" çalıştığını not ediyorum. Bu işlevler niteliksel olarak farklı kavramları temsil etmez. Zaman uyumsuz işlevler, vaatleri kullanan koda alternatif olarak tasarlanmıştır.

Async/Await, asenkron kodla çalışmayı senkronize bir tarzda düzenlemeyi mümkün kılar.

Böylece vaatleri bilmek Async/Await ilkelerini anlamayı kolaylaştırır.

sözdizimi

Normalde iki anahtar kelimeden oluşur: async ve wait. İlk kelime işlevi eşzamansız hale getirir. Bu tür işlevler wait kullanımına izin verir. Aksi takdirde, bu fonksiyonun kullanılması bir hata üretecektir.

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

Async, işlev bildiriminin en başına ve bir ok işlevi durumunda “=” işareti ile parantezlerin arasına eklenir.

Bu işlevler bir nesneye yöntem olarak yerleştirilebilir veya bir sınıf bildiriminde kullanılabilir.

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

Dikkat! Sınıf yapıcılarının ve alıcı/ayarlayıcılarının eşzamansız olamayacağını hatırlamakta fayda var.

Anlambilim ve yürütme kuralları

Zaman uyumsuz işlevler temel olarak standart JS işlevlerine benzer, ancak istisnalar da vardır.

Bu nedenle, eşzamansız işlevler her zaman vaatleri döndürür:

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

Özellikle, fn merhaba dizesini döndürür. Bu eşzamansız bir işlev olduğundan, dize değeri bir yapıcı kullanılarak bir sözle sarılır.

İşte Async'siz alternatif bir tasarım:

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

Bu durumda söz “manuel olarak” iade edilir. Eşzamansız bir işlev her zaman yeni bir sözle sarılır.

Dönüş değeri bir ilkel ise, eşzamansız işlev, değeri bir sözle sararak döndürür. Dönüş değeri bir söz nesnesiyse, çözümü yeni bir sözle döndürülür.

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

Peki eşzamansız bir işlevin içinde bir hata varsa ne olur?

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

İşlenmezse foo(), reddedilen bir söz döndürecektir. Bu durumda Promise.resolve yerine hata içeren Promise.reject döndürülür.

Zaman uyumsuz işlevler, ne döndürüldüğüne bakılmaksızın her zaman bir söz verir.

Eşzamansız işlevler her beklemede duraklatılır.

Beklemek ifadeleri etkiler. Yani eğer ifade bir söz ise, söz yerine getirilene kadar eşzamansız işlev askıya alınır. İfade bir söz değilse Promise.resolve aracılığıyla söze dönüştürülür ve daha 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);

Ve burada fn fonksiyonunun nasıl çalıştığının bir açıklaması var.

  • Çağırdıktan sonra ilk satır const a = wait 9'a dönüştürülür; const a = wait Promise.resolve(9);
  • Await'i kullandıktan sonra, a değerini alana kadar işlevin yürütülmesi askıya alınır (mevcut durumda 9'dur).
  • DelayAndGetRandom(1000), fn işlevinin yürütülmesini, kendisi tamamlanana kadar (1 saniye sonra) duraklatır. Bu, fn işlevini 1 saniyeliğine etkili bir şekilde durdurur.
  • DelayAndGetRandom(1000) çözüm yoluyla rastgele bir değer döndürür ve bu değer daha sonra b değişkenine atanır.
  • C değişkenli durum, a değişkenli duruma benzer. Bundan sonra her şey bir saniyeliğine durur, ancak artık gecikmeAndGetRandom(1000) gerekli olmadığından hiçbir şey döndürmez.
  • Sonuç olarak değerler a + b * c formülü kullanılarak hesaplanır. Sonuç, Promise.resolve kullanılarak bir söze sarılır ve işlev tarafından döndürülür.

Bu duraklamalar ES6'daki jeneratörleri anımsatıyor olabilir ama bunda bir şeyler var senin sebeplerin.

Sorunu çözmek

Peki şimdi yukarıda bahsettiğimiz sorunun çözümüne bakalım.

FinishMyTask işlevi, queryDatabase, sendEmail, logTaskInFile ve diğerleri gibi işlemlerin sonuçlarını beklemek için Await'i kullanır. Bu çözümü vaatlerin kullanıldığı çözümle karşılaştırırsanız benzerlikler açıkça ortaya çıkacaktır. Ancak Async/Await sürümü tüm sözdizimsel karmaşıklıkları büyük ölçüde basitleştirir. Bu durumda .then/.catch gibi çok sayıda geri arama ve zincir yoktur.

İşte sayıların çıktısıyla bir çözüm, iki seçenek 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);
    });
  });
};

Ve işte eşzamansız işlevleri kullanan bir çözüm.

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

Hata işleme

İşlenmeyen hatalar reddedilen bir söze sarılır. Ancak eşzamansız işlevler, hataları eşzamanlı olarak işlemek için try/catch'i kullanabilir.

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(), başarılı olan ("mükemmel sayı") veya bir hatayla başarısız olan ("Üzgünüm, sayı çok büyük") eş zamanlı olmayan bir işlevdir.

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

Yukarıdaki örnek canRejectOrReturn'un yürütülmesini beklediğinden, kendi hatası catch bloğunun yürütülmesine neden olacaktır. Sonuç olarak, foo işlevi ya tanımsız (try bloğunda hiçbir şey döndürülmediğinde) ya da bir hatanın yakalanmasıyla sona erecektir. Sonuç olarak, bu işlev başarısız olmayacaktır çünkü try/catch foo işlevinin kendisini yönetecektir.

İşte başka bir örnek:

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

Örnekte canRejectOrReturn'un foo'dan döndürüldüğüne dikkat etmek önemlidir. Bu durumda Foo ya mükemmel bir sayıyla sonlanır ya da bir Hata döndürür (“Üzgünüm, sayı çok büyük”). Catch bloğu hiçbir zaman çalıştırılmayacaktır.

Sorun foo'nun canRejectOrReturn'dan aktarılan vaadi döndürmesidir. Böylece foo'nun çözümü, canRejectOrReturn'un çözümü haline gelir. Bu durumda kod yalnızca iki satırdan oluşacaktır:

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

Eğer wait ve return'ü birlikte kullanırsanız ne olur:

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

Yukarıdaki kodda foo hem mükemmel bir sayı hem de bir hata yakalanarak başarıyla çıkacaktır. Burada herhangi bir ret olmayacak. Ancak foo, unDefinition ile değil, canRejectOrReturn ile geri dönecektir. Return wait canRejectOrReturn() satırını kaldırarak bundan emin olalım:

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

Yaygın hatalar ve tuzaklar

Bazı durumlarda Async/Await'in kullanılması hatalara neden olabilir.

Unutulmuş bekleyiş

Bu oldukça sık olur - wait anahtar sözcüğü sözden önce unutulur:

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

Gördüğünüz gibi kodda wait veya return yok. Bu nedenle foo her zaman 1 saniyelik bir gecikme olmadan tanımsız olarak çıkar. Ama söz yerine getirilecek. Bir hata veya ret durumunda UnhandledPromiseRejectionWarning çağrılacaktır.

Geri Aramalarda Eşzamansız İşlevler

Zaman uyumsuz işlevler, .map veya .filter'da geri arama olarak oldukça sık kullanılır. Bunun bir örneği, GitHub'daki açık depoların sayısını döndüren fetchPublicReposCount(username) işlevidir. Diyelim ki metriklerine ihtiyacımız olan üç kullanıcı var. İşte bu görevin kodu:

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

ArfatSalman, octocat, norvig hesaplarına ihtiyacımız var. Bu durumda şunları yaparız:

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

.map geri aramasında Await'e dikkat etmeye değer. Burada counts bir dizi sözdür ve .map ise belirtilen her kullanıcı için anonim bir geri aramadır.

Beklemenin aşırı tutarlı kullanımı

Örnek olarak bu kodu ele alalım:

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 numarası count değişkenine yerleştiriliyor, daha sonra bu sayı counts dizisine ekleniyor. Kodun sorunu, sunucudan ilk kullanıcının verileri gelene kadar sonraki tüm kullanıcıların bekleme modunda olmasıdır. Böylece aynı anda yalnızca bir kullanıcı işlenir.

Örneğin, bir kullanıcıyı işlemek yaklaşık 300 ms sürüyorsa, bu tüm kullanıcılar için zaten bir saniyedir; harcanan süre doğrusal olarak kullanıcı sayısına bağlıdır. Ancak repo sayısının elde edilmesi birbirine bağlı olmadığı için süreçler paralelleştirilebilmektedir. Bu, .map ve Promise.all ile çalışmayı gerektirir:

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

Promise.all girdi olarak bir dizi söz alır ve bir söz döndürür. İkincisi, dizideki tüm sözler yerine getirildikten sonra veya ilk reddedildiğinde tamamlanır. Hepsi aynı anda başlamayabilir; eşzamanlı başlatmayı sağlamak için p-map'i kullanabilirsiniz.

Sonuç

Zaman uyumsuz işlevler geliştirme açısından giderek daha önemli hale geliyor. Eşzamansız işlevlerin uyarlanabilir kullanımı için kullanmaya değer Eşzamansız Yineleyiciler. Bir JavaScript geliştiricisinin bu konuda bilgili olması gerekir.

Skillbox şunları önerir:

Kaynak: habr.com

Yorum ekle