Fungsi asinkron: membuat promise lebih ramah

Fungsi asinkron memungkinkan Anda menulis kode berbasis promise seolah-olah kode tersebut merupakan kode sinkron.

Jake Archibald
Jake Archibald

Fungsi asinkron diaktifkan secara default di Chrome, Edge, Firefox, dan Safari, dan fungsi tersebut cukup luar biasa. Fungsi ini memungkinkan Anda menulis kode berbasis promise seakan-akan sinkron, tetapi tanpa memblokir thread utama. Fungsi ini membuat kode asinkron Anda kurang "cerdas" dan lebih mudah dibaca.

Fungsi asinkron bekerja seperti ini:

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

Jika Anda menggunakan kata kunci async sebelum definisi fungsi, Anda dapat menggunakan await dalam fungsi. Saat Anda await promise, fungsi akan dijeda dengan cara yang tidak memblokir hingga promise selesai. Jika promise terpenuhi, Anda akan mendapatkan kembali nilai tersebut. Jika promise ditolak, nilai yang ditolak akan dibuang.

Dukungan browser

Dukungan Browser

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

Sumber

Contoh: mencatat pengambilan

Misalnya, Anda ingin mengambil URL dan membuat log respons sebagai teks. Berikut adalah tampilannya menggunakan promise:

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

Dan berikut adalah fungsi yang sama dengan menggunakan fungsi asinkron:

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

Mempunyai jumlah baris yang sama, namun semua callback menghilang. Hal ini membuat membaca jauh lebih mudah, terutama bagi mereka yang belum familier dengan promise.

Nilai yang ditampilkan asinkron

Fungsi asinkron selalu menampilkan promise, baik Anda menggunakan await atau tidak. Promise tersebut diselesaikan dengan fungsi asinkron apa pun yang ditampilkan, atau ditolak dengan fungsi asinkron apa pun yang ditampilkan. Jadi dengan:

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

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

…memanggil hello() akan menampilkan promise yang memenuhi dengan "world".

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

…memanggil foo() akan menampilkan promise yang menolak dengan Error('bar').

Contoh: streaming respons

Keuntungan fungsi asinkron semakin bertambah pada contoh yang lebih kompleks. Misalnya, Anda ingin memutar respons sembari logout dari potongan, dan menampilkan ukuran akhir.

Berikut tampilan dengan promise:

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

Lihat saya, Jake "wielder of promises" Archibald. Lihat bagaimana saya memanggil processResult() di dalamnya sendiri untuk menyiapkan loop asinkron? Menulis hal itu membuat saya merasa sangat cerdas. Namun, seperti kebanyakan kode "cerdas", Anda harus menatapnya dalam waktu yang lama untuk mencari tahu apa yang dilakukannya, seperti salah satu gambar mata-ajaib dari tahun 90-an.

Mari kita coba lagi dengan fungsi asinkron:

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

Semua "kecerdasan" hilang. Loop asinkron yang membuat saya merasa begitu puas diganti dengan while-loop yang terpercaya dan membosankan. Jauh lebih baik. Di masa mendatang, Anda akan mendapatkan iterator asinkron, yang akan mengganti loop while dengan loop for-of, sehingga lebih rapi.

Sintaksis fungsi asinkron lainnya

Kami telah menunjukkan async function() {} kepada Anda, tetapi kata kunci async dapat digunakan dengan sintaksis fungsi lain:

Fungsi panah

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

Metode objek

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

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

Metode class

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

Hati-hati! Hindari terlalu sekuensial

Meskipun Anda menulis kode yang terlihat sinkron, pastikan Anda tidak melewatkan kesempatan untuk melakukan hal-hal secara paralel.

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

Fungsi di atas membutuhkan waktu 1000 md untuk diselesaikan, sedangkan:

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

Fungsi di atas membutuhkan waktu 500 md untuk diselesaikan, karena kedua proses tunggu terjadi pada waktu yang sama. Mari kita lihat contoh praktisnya.

Contoh: menampilkan pengambilan secara urut

Misalnya, Anda ingin mengambil serangkaian URL dan membuat log sesegera mungkin, dengan urutan yang benar.

Tarik napas dalam-dalam - Beginilah tampilannya dengan promise:

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

Ya, benar, saya menggunakan reduce untuk merangkai urutan promise. Saya sangat pintar. Namun, ini adalah pengkodean sangat cerdas yang sebaiknya tidak Anda gunakan.

Namun, saat mengonversi kode di atas menjadi fungsi asinkron, Anda mungkin tergoda untuk melakukannya terlalu berurutan:

Tidak direkomendasikan - terlalu sekuensial
async function logInOrder(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}
Terlihat jauh lebih rapi, tetapi pengambilan kedua saya tidak dimulai sampai pengambilan pertama saya telah dibaca sepenuhnya, dan seterusnya. Ini jauh lebih lambat dibandingkan contoh promise yang melakukan pengambilan secara paralel. Untungnya ada jalan tengah yang ideal.
Direkomendasikan - bagus dan 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);
  }
}
Dalam contoh ini, URL diambil dan dibaca secara paralel, tetapi bit reduce "cerdas" diganti dengan for-loop standar, membosankan, dan dapat dibaca.

Solusi dukungan browser: generator

Jika Anda menargetkan browser yang mendukung generator (yang mencakup versi terbaru dari setiap browser utama ), Anda dapat mengurutkan fungsi asinkron polyfill.

Babel akan melakukannya untuk Anda, berikut adalah contoh melalui Babel REPL

  • perhatikan bagaimana miripnya kode yang ditranspilasi. Transformasi ini adalah bagian dari preset es2017 Babel.

Sebaiknya gunakan pendekatan transpiling, karena Anda dapat menonaktifkannya setelah browser target mendukung fungsi asinkron, tetapi jika Anda benar-benar tidak ingin menggunakan transpiler, Anda dapat menggunakan polyfill Babel dan menggunakannya sendiri. Daripada:

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

…Anda akan menyertakan polyfill dan menulis:

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

Perhatikan bahwa Anda harus meneruskan generator (function*) ke createAsyncFunction, dan menggunakan yield, bukan await. Selain itu, semuanya sama.

Solusi: regenerator

Jika Anda menargetkan browser lama, Babel juga dapat melakukan transpile generator, memungkinkan Anda menggunakan fungsi asinkron hingga sampai IE8. Untuk melakukannya, Anda memerlukan preset es2017 Babel dan preset es2015.

Outputnya tidak semenarik, jadi waspadalah terhadap code-bloat.

Lakukan async ke semua hal!

Setelah fungsi asinkron tersedia di semua browser, gunakan fungsi tersebut pada setiap fungsi yang menampilkan promise. Fungsi ini tidak hanya membuat kode Anda lebih rapi, tetapi juga memastikan bahwa fungsi tersebut akan selalu menampilkan promise.

Saya benar-benar bersemangat tentang fungsi asinkron pada tahun 2014, dan sangat senang bisa melihatnya hadir, secara nyata, dalam browser. Whoop!