Eş zamansız işlevler: Vaatleri anlaşılır hale getirme

Asynchronize işlevler, promise tabanlı kodları zaman uyumluymuş gibi yazmanıza olanak tanır.

Jake Archibald
Jake Archibald

Asenkron işlevler Chrome, Edge, Firefox ve Safari'de varsayılan olarak etkindir ve gerçekten muhteşemdir. Bu işlevler, ana iş parçacığı engellenmeden, söze dayalı kodları sanki senkronizeymiş gibi yazmanıza olanak tanır. Bunlar, eşzamansız kodunuzu daha az "zekice" ve daha okunabilir hale getirir.

Eş zamansız işlevler şu şekilde çalışır:

async function myFirstAsyncFunction() {
  try {
    const fulfilledValue = await promise;
  } catch (rejectedValue) {
    // …
  }
}

Bir işlev tanımından önce async anahtar kelimesini kullanırsanız işlev içinde await kullanabilirsiniz. Bir vaadi await istediğinizde işlev, vaat yerine gelene kadar engellemeyen bir şekilde duraklatılır. Söz verilen işlem gerçekleşirse değeri geri alırsınız. Sözleşme reddedilirse reddedilen değer atılır.

Tarayıcı desteği

Tarayıcı desteği

  • Chrome: 55.
  • Kenar: 15.
  • Firefox: 52.
  • Safari: 10.1.

Kaynak

Örnek: bir getirme işlemini günlüğe kaydetme

Bir URL almak ve yanıtı metin olarak günlüğe kaydetmek istediğinizi varsayalım. Söz vererek bu işlemi aşağıdaki gibi yapabilirsiniz:

function logFetch(url) {
  return fetch(url)
    .then((response) => response.text())
    .then((text) => {
      console.log(text);
    })
    .catch((err) => {
      console.error('fetch failed', err);
    });
}

Aşağıda, aynı işlemler asynkron işlevler kullanılarak gösterilmiştir:

async function logFetch(url) {
  try {
    const response = await fetch(url);
    console.log(await response.text());
  } catch (err) {
    console.log('fetch failed', err);
  }
}

Satır sayısı aynıdır ancak tüm geri aramalar kaldırılmıştır. Bu sayede, özellikle de vaatler hakkında daha az bilgi sahibi olanlar için okumak çok daha kolay olur.

Eşzamansız döndürülen değerler

await kullanıp kullanmadığınıza bakılmaksızın, asynkron işlevler her zaman bir promise döndürür. Bu söz, asynkron işlevin döndürdüğü değerle çözülür veya asynkron işlevin oluşturduğu hatayla reddedilir. Dolayısıyla:

// wait ms milliseconds
function wait(ms) {
  return new Promise((r) => setTimeout(r, ms));
}

async function hello() {
  await wait(500);
  return 'world';
}

hello() çağrısı, "world" ile yerine getirilen bir söz döndürür.

async function foo() {
  await wait(500);
  throw Error('bar');
}

foo() çağrısı, Error('bar') ile reddedilen bir promise döndürür.

Örnek: Yanıtı akış şeklinde gönderme

Asenkron işlevlerin avantajı, daha karmaşık örneklerde artar. Parçaları günlüğe kaydederken bir yanıtı aktarmak ve nihai boyutu döndürmek istediğinizi varsayalım.

Sözlerimizi yerine getiriyoruz:

function getResponseSize(url) {
  return fetch(url).then((response) => {
    const reader = response.body.getReader();
    let total = 0;

    return reader.read().then(function processResult(result) {
      if (result.done) return total;

      const value = result.value;
      total += value.length;
      console.log('Received chunk', value);

      return reader.read().then(processResult);
    });
  });
}

Ben Jake "söz veren" Archibald. Asenkron bir döngü oluşturmak için processResult() işlevini kendi içinde nasıl çağırdığımı görüyor musunuz? Bu yazı beni çok akıllı hissettirdi. Ancak çoğu "akıllı" kodda olduğu gibi, ne yaptığını anlamak için yaşlarca ona bakmanız gerekiyor.

Bunu tekrar asenkron işlevlerle deneyelim:

async function getResponseSize(url) {
  const response = await fetch(url);
  const reader = response.body.getReader();
  let result = await reader.read();
  let total = 0;

  while (!result.done) {
    const value = result.value;
    total += value.length;
    console.log('Received chunk', value);
    // get the next result
    result = await reader.read();
  }

  return total;
}

"Akıllı" olan her şey kayboldu. Kendimi çok iyi hissetmeme neden olan asenkron döngü, güvenilir ve sıkıcı bir while döngüsü ile değiştirildi. şimdi daha iyi oldu. Gelecekte, while döngüsünü for-of döngüsüyle değiştirerek daha da düzenli hale getirecek asynchronize iteratörlere sahip olacaksınız.

Diğer eş zamansız işlev söz dizimi

async function() {}'ü daha önce gösterdim ancak async anahtar kelimesi diğer işlev söz dizimi ile de kullanılabilir:

Ok işlevleri

// map some URLs to json-promises
const jsonPromises = urls.map(async (url) => {
  const response = await fetch(url);
  return response.json();
});

Nesne yöntemleri

const storage = {
  async getAvatar(name) {
    const cache = await caches.open('avatars');
    return cache.match(`/avatars/${name}.jpg`);
  }
};

storage.getAvatar('jaffathecake').then(…);

Sınıf yöntemleri

class Storage {
  constructor() {
    this.cachePromise = caches.open('avatars');
  }

  async getAvatar(name) {
    const cache = await this.cachePromise;
    return cache.match(`/avatars/${name}.jpg`);
  }
}

const storage = new Storage();
storage.getAvatar('jaffathecake').then(…);

Dikkatli olun. Çok sıralı bir şekilde ilerlemekten kaçının

Senkronize görünen kodlar yazsanız da işlemleri paralel olarak yapma fırsatını kaçırmayın.

async function series() {
  await wait(500); // Wait 500ms…
  await wait(500); // …then wait another 500ms.
  return 'done!';
}

Yukarıdaki işlemin tamamlanması 1.000 ms sürer. Buna karşılık:

async function parallel() {
  const wait1 = wait(500); // Start a 500ms timer asynchronously…
  const wait2 = wait(500); // …meaning this timer happens in parallel.
  await Promise.all([wait1, wait2]); // Wait for both timers in parallel.
  return 'done!';
}

Yukarıdaki işlemin tamamlanması 500 ms sürer çünkü her iki bekleme de aynı anda gerçekleşir. Pratik bir örneğe göz atalım.

Örnek: Getirme işlemlerini sırayla yayınlama

Bir dizi URL'yi almak ve en kısa sürede doğru sırayla günlüğe kaydetmek istediğinizi varsayalım.

Derin nefes - Vaat edilen sözler şu şekilde görünür:

function markHandled(promise) {
  promise.catch(() => {});
  return promise;
}

function logInOrder(urls) {
  // fetch all the URLs
  const textPromises = urls.map((url) => {
    return markHandled(fetch(url).then((response) => response.text()));
  });

  // log them in order
  return textPromises.reduce((chain, textPromise) => {
    return chain.then(() => textPromise).then((text) => console.log(text));
  }, Promise.resolve());
}

Doğru duydunuz, bir dizi vaat için reduce kullanıyorum. Çok zekiyim. Ancak bu kod biraz çok akıllı olduğu için yazmadan daha iyi olacak.

Ancak yukarıdakileri ayarsız bir işleve dönüştürürken çok sıralı bir yaklaşım benimsemek cazip gelebilir:

Önerilmez - çok sıralı
async function logInOrder(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}
Daha düzenli görünüyor ancak ikinci getirme işlemim, ilk getirme işlemim tamamen okunana kadar başlamaz. Bu, getirme işlemlerini paralel olarak gerçekleştiren promises örneğinden çok daha yavaştır. Neyse ki ideal bir orta yol var.
Önerilen: Güzel ve paralel
function markHandled(...promises) {
  Promise.allSettled(promises);
}

async function logInOrder(urls) {
  // fetch all the URLs in parallel
  const textPromises = urls.map(async (url) => {
    const response = await fetch(url);
    return response.text();
  });

  markHandled(...textPromises);

  // log them in sequence
  for (const textPromise of textPromises) {
    console.log(await textPromise);
  }
}
Bu örnekte, URL'ler paralel olarak getirilip okunur. Ancak "akıllı" reduce bitinin yerini standart, sıkıcı ve okunabilir bir döngüyle değiştirilmiştir.

Tarayıcı desteği geçici çözümü: jeneratörler

Oluşturucuları destekleyen tarayıcıları (tüm büyük tarayıcıların en son sürümü dahil) hedefliyorsanız asenkron işlevleri bir nevi çoklu doldurabilirsiniz.

Babel bunu sizin için yapar. Babel REPL üzerinden bir örnek

Hedef tarayıcıları senkronize olmayan işlevleri destekledikten sonra devre dışı bırakabileceğiniz için derleme yaklaşımını öneririm. Ancak gerçekten derleyici kullanmak istemiyorsanız Babel'in polyfill'ini alıp kendiniz kullanabilirsiniz. Bunun yerine:

async function slowEcho(val) {
  await wait(1000);
  return val;
}

polyfill'i dahil eder ve şunları yazarsınız:

const slowEcho = createAsyncFunction(function* (val) {
  yield wait(1000);
  return val;
});

createAsyncFunction için bir oluşturucu (function*) iletmeniz ve await yerine yield kullanmanız gerektiğini unutmayın. Bunun dışında aynı şekilde çalışır.

Geçici çözüm: yenileyici

Eski tarayıcıları hedefliyorsanız Babel, üreteçleri de aktarabilir. Böylece, IE8'e kadar olan tüm tarayıcılarda asenkron işlevleri kullanabilirsiniz. Bunun için Babel'in es2017 hazır ayarına ve es2015 hazır ayarına ihtiyacınız vardır.

Çıkış o kadar güzel değildir. Bu nedenle kod şişmesine dikkat edin.

Her şeyi eş zamansız yapabilirsiniz.

Asenkron işlevler tüm tarayıcılarda kullanıma sunulduğunda, bunları her bir promise döndüren işlevde kullanın. Bunlar, kodunuzu daha düzenli hale getirmekle kalmaz, işlevin her zaman bir promise döndürmesini sağlar.

2014'te asenkron işlevler hakkında çok heyecanlanmıştım ve bunların tarayıcılarda gerçekten kullanıma sunulduğunu görmek harika. Oley!