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

Eş zamansız işlevler, sanki eşzamanlı olan vaadi tabanlı kodlar yazmanıza imkan tanır.

Jake Archibald
Jake Archibald

Chrome, Edge, Firefox ve Safari'de eş zamansız işlevler varsayılan olarak etkindir ve açıkçası oldukça harikadır. Eşzamanlıymış gibi, ancak ana iş parçacığını engellemeden tarihe dayalı kod yazmanıza imkan tanırlar. Bunlar, eşzamansız kodunuzu daha az "zeki" ve daha okunabilir hale getirir.

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

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

async anahtar kelimesini bir işlev tanımından önce kullanırsanız işlev içinde await kullanabilirsiniz. Bir sözü await verdiğinizde söz konusu olana kadar işlev engelleme olmadan duraklatılır. Vaadiniz karşılanırsa değeri geri alırsınız. Vaat reddedilirse, reddedilen değer reddedilir.

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 ve yanıtı metin olarak günlüğe kaydetmek istediğinizi düşünelim. Vaatler kullanıldığında şu şekilde 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);
    });
}

Eş zamansız fonksiyonların kullanımında da aynı durum söz konusudur:

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 kullanır, ancak tüm geri çağırma işlemleri iptal edilir. Bu da, özellikle vaatler konusuna aşina olmayanların okumasını kolaylaştırır.

Eş zamansız dönüş değerleri

await uygulamasını kullansanız da kullanmasanız da eş zamansız işlevler her zaman bir söz döndürür. Bu söz, eşzamansız işlevin döndürdüğü her şeyle çözümlenir veya eşzamansız işlevin döndürdüğü şeyle reddedilir. Yani şunun için:

// 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 karşılayan bir söz döndürür.

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

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

Örnek: yanıt akışı

Karmaşık örneklerde eşzamansız işlevlerin avantajı daha fazladır. Parçalardan çıkış yaparken bir yanıtı akış olarak yayınlamak ve son boyutu döndürmek istediğinizi varsayalım.

İşte vaatler:

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

Bana göz atın, Jake "vaatlerin sahibi" Archibald. Eşzamansız bir döngü oluşturmak için kendi içinde processResult()'i nasıl çağırdığımı görüyor musunuz? Çok akıllı hissettiren yazılar. Ancak çoğu "akıllı" kodda olduğu gibi, ne yaptığını anlamak için ona yıllarca bakmanız gerekiyor. Örneğin, 90'lardaki sihirli göz resimlerden biri.

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 geride kaldı. Kendimi beğenmiş hissetmemi sağlayan eşzamansız döngü yerine güvenilir, sıkıcı, sıra dışı bir döngü geldi. şimdi daha iyi oldu. Gelecekte, while döngüsünü bir for-of döngü ile değiştirecek ve daha da düzenli hale getirecek eşzamansız yinelemeler edineceksiniz.

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

Size async function() {} bağlantısını zaten gösterdik ancak async anahtar kelimesi başka işlev söz dizimiyle 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ı bir geçişten kaçının

Eşzamanlı görünen kod yazıyor olsanız da işlemleri paralel olarak 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. Bununla birlikte:

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 aynı anda gerçekleştiğinden yukarıdaki işlemin tamamlanması 500 ms sürer. Şimdi bir örnek inceleyelim.

Örnek: Getirmelerin çıktısını sırayla alma

Bir dizi URL'yi getirmek ve bunları mümkün olan en kısa sürede doğru sırada günlüğe kaydetmek istediğinizi varsayalım.

Derin nefes: İşte vadedilenler:

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

Evet, doğru. Bir dizi vaatte bulunmak için reduce kullanıyorum. Çok akıllıyım. Ancak bu, kod kullanmadan daha iyi sonuç alabileceğiniz akıllı bir kodlamadır.

Ancak, yukarıdakileri eşzamansız bir işleve dönüştürürken çok sıralı seçeneği tercih edebilirsiniz:

Ö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üzgün görünüyor, ancak ilk getirmem tamamen okununcaya kadar ikinci getirme işlemim başlamıyor. Bu, getirme işlemlerini paralel olarak gerçekleştiren Prova örneğinden çok daha yavaştır. Neyse ki ideal bir orta yol var.
Önerilir - 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 getirilir ve paralel olarak okunur. Ancak "smart" reduce biti standart, sıkıcı, okunabilir bir for-loop ile değiştirilir.

Tarayıcı desteği geçici çözümü: oluşturucular

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

Babel bunu sizin yerinize yapacaktır. Babel REPL'den alınan bir örneği burada bulabilirsiniz.

Aktarma yaklaşımını kullanmanızı öneririm, çünkü hedef tarayıcılarınız eş zamansız işlevleri desteklediğinde bunu kapatabilirsiniz, ancak gerçekten bir aktarıcı kullanmak istemiyorsanız Babel'in çoklu dolgusunu alıp kendiniz kullanabilirsiniz. Şunun yerine:

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

Çoklu dolguyu eklemeniz ve şunu yazmanız gerekir:

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

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

Geçici çözüm: regenerator

Daha eski tarayıcıları hedefliyorsanız Babel, oluşturucuları da aktarabilirsiniz. Böylece, eş zamansız işlevleri IE8'e kadar kullanabilirsiniz. Bunun için Babel es2017 hazır ayarına ve es2015 hazır ayarına ihtiyacınız vardır.

Çıkış çok hoş değildir, bu nedenle kod şişmesine dikkat edin.

Eş zamansız her şey!

Eş zamansız işlevler tüm tarayıcılara ulaştığında, bunları tüm vaat eden işlevlerde kullanın. Hem kodunuzu daha düzenli hale getirir hem de işlevin her zaman bir vaat getirmesini sağlar.

2014 yılında eşzamansız işlevler konusunda gerçekten çok heyecanlandım ve bunların gerçek olarak tarayıcılara ulaştığını görmek çok güzel. Tüh!