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

Eşzamansız işlevler, eş zamanlıymış gibi taahhüt tabanlı kodlar yazmanıza olanak tanır.

Jake Archibald
Jake Archibald

Chrome, Edge, Firefox ve Safari'de varsayılan olarak etkinleştirilen eşzamansız işlevler son derece muhteşemdir. Ana iş parçacığını engellemeden, eş zamanlıymış gibi taahhüde dayalı kod yazmanıza olanak tanırlar. Bunlar, eşzamansız kodunuzu daha az "akıllı" ve daha okunabilir hale getirir.

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

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

İş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 edilenler gerçekleşirse değeri geri alırsınız. Vaat reddedilirse reddedilen değer atılır.

Tarayıcı desteği

Tarayıcı Desteği

  • 55
  • 15
  • 52
  • 10.1

Kaynak

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

Bir URL getirmek istediğinizi ve yanıtı metin olarak günlüğe kaydetmek istediğinizi düşünelim. Vaatler kullanıldığında şöyle görünür:

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

Burada da aynı şeyi eşzamansız işlevlerin kullanıldığı açıklayalım:

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

Aynı sayıda satır var ancak tüm geri çağırmalar kayboldu. Bu da özellikle vaatlere aşina olmayanların okumasını çok kolaylaştırıyor.

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

await kullansanız da kullanmasanız da eşzamansız işlevler her zaman belirli bir değer döndürür. Bu durum, eş zamansız işlevin döndürdüğü işlemle çözümlenir veya eşzamansız işlevin attığı her işlemle 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ının yapılması, "world" ile gerçekleştirileceği bir sözü döndürür.

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

...foo() çağrısı yapıldığında Error('bar') ile beraber reddeden bir söz döndürülür.

Örnek: yanıt akışı sağlama

Daha karmaşık örneklerde eşzamansız işlevlerin avantajı daha da artar. Parçalardan çıkış yaparken bir yanıtı akış olarak yayınlamak istediğinizi ve son boyutu döndürmek istediğinizi düşünelim.

Sözlerle şöyle:

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

Şuna bir göz atın: Jake "vaatlerin sahibi" Archibald. Eşzamansız döngü oluşturmak için processResult() öğesini kendi içinde nasıl çağırdığıma bakın. Yazmak bana çok akıllı hissettirdi. Ancak çoğu "akıllı" kod gibi, ne yaptığını anlamak için yaşlarca ona bakmanız gerekiyor.

Bunu eşzamansız işlevlerle tekrar 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;
}

Tüm "akıllı" özellikler sona erdi. Kendimi bu kadar kibirli hissettiren eşzamansız döngünün yerini güvenilir, sıkıcı ve döngü döngüsünü aldı. şimdi daha iyi oldu. Gelecekte, while döngüsünü bir "for-of döngüsüyle" değiştirerek döngüyü daha da düzgün hale getiren eş zamansız iteratörler oluşturacaksınız.

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

Size daha önce async function() {} anahtar kelimesini göstermiştim, ancak async anahtar kelimesi başka işlev söz dizimiyle birlikte 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 ol! Çok sıralı ifadeler kullanmaktan kaçının

Eşzamanlı görünen kodlar yazsanız da paralel olarak işlem yapma fırsatını kaçırmadığınızdan emin olun.

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

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

Her iki bekleme işlemi aynı anda gerçekleştiğinden yukarıdaki işlemin tamamlanması 500 ms sürer. Pratik bir örneğe göz atalım.

Örnek: Getirme işlemlerini sırasıyla çıkarma

Bir dizi URL getirmek ve bunları mümkün olan en kısa sürede, doğru sırayla 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 eşzamansız bir işleve dönüştürürken çok sıralı hale gelmek 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());
  }
}
Çok daha düzenli görünüyor ancak ilk getirme işlemim tamamen okununcaya kadar ikinci getirme işlemim başlamaz ve bu şekilde devam eder. Bu, getirme işlemlerini paralel olarak gerçekleştiren vaat ö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ü: oluşturucular

Oluşturucuları (her önemli tarayıcının en son sürümünü içerir) destekleyen tarayıcıları hedefliyorsanız çoklu dolgu eşzamansız işlevleri sıralayabilirsiniz.

Babel bunu sizin için yapar. Aşağıda, Babel REPL üzerinden bir örnek verilmiştir.

Dönüştürme yaklaşımını kullanmanızı öneririz, çünkü hedef tarayıcılarınız eşzamansız işlevleri desteklediğinde bu özelliği kapatabilirsiniz. Ancak gerçekten bir aktarıcı kullanmak istemiyorsanız Babel'ın çoklu dolgusunu alıp kendiniz kullanabilirsiniz. Şunun yerine:

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

...çoklu dolguyu ekler ve şöyle yazarsınız:

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

createAsyncFunction öğesine 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: yeniden oluşturucu

Eski tarayıcıları hedefliyorsanız Babel, oluşturucuları da aktarıp eşzamansız işlevleri IE8'e kadar kullanmanıza olanak tanır. Bunun için Babel'ın es2017 hazır ayarına ve es2015 hazır ayarına ihtiyacınız vardır.

Çıkış o kadar güzel değil, bu yüzden fazla koda dikkat edin.

Her şeyi eş zamansız olarak yapın.

Eş zamansız işlevler tüm tarayıcılarda görüntülendiğinde, bunları vaat getiren her işlevde kullanın. Bunlar hem kodunuzu daha düzenli hale getirir hem de işlevin her zaman bir vaat döndürmesini sağlar.

2014'te eşzamansız işlevler konusunda gerçekten çok heyecanlanmıştım ve bu işlevlerin gerçek tarayıcılarda tarayıcılarla karşılandığını görmek harika. Hay aksi!