Fungsi asinkron: membuat promise lebih ramah

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

Jake Archibald
Jake Archibald

Fungsi asinkron diaktifkan secara default di Chrome, Edge, Firefox, dan Safari, serta dan sejujurnya mereka sangat luar biasa. Mereka memungkinkan Anda untuk menulis kode berbasis promise sebagai jika dilakukan secara sinkron, tetapi tanpa memblokir thread utama. Hal tersebut menjadikan kode asinkron kurang "cerdas" dan lebih mudah dibaca.

Fungsi asinkron bekerja seperti ini:

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

Jika menggunakan kata kunci async sebelum definisi fungsi, Anda dapat menggunakan await dalam fungsi. Saat Anda melakukan await pada promise, fungsi tersebut akan dijeda dengan cara yang tidak memblokir sampai promise selesai. Jika janji itu terpenuhi, Anda mendapatkan kembali nilainya. Jika promise ditolak, nilai yang ditolak akan ditampilkan.

Dukungan browser

Dukungan Browser

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

Sumber

Contoh: mencatat pengambilan ke dalam log

Misalnya Anda ingin mengambil URL dan mencatat respons sebagai teks. Berikut 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);
  }
}

Jumlah barisnya sama, tetapi semua callback hilang. Hal ini menunjukkan lebih mudah dibaca, terutama bagi mereka yang kurang familier dengan promise.

Nilai yang ditampilkan asinkron

Fungsi asinkron selalu menampilkan promise, baik Anda menggunakan await atau tidak. Bahwa promise tersebut akan di-resolve dengan fungsi async apa pun yang ditampilkan, atau ditolak dengan fungsi async apa pun yang dimunculkan. 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() menampilkan promise yang memenuhi dengan "world".

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

...memanggil foo() menampilkan promise yang ditolak dengan Error('bar').

Contoh: menstreaming respons

Manfaat fungsi asinkron meningkat dalam contoh yang lebih kompleks. Misalnya Anda ingin untuk melakukan {i>streaming <i}respons sambil {i>log out <i}dari potongan, dan mengembalikan ukuran akhir.

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

Coba lihat, Jake "wielder of promises" (pembawa janji) Diarsipkan. Lihat cara saya menelepon processResult() di dalam dirinya sendiri untuk menyiapkan loop asinkron? Tulisan yang menghasilkan saya merasa sangat pintar. Tapi seperti kebanyakan "cerdas" {i>code<i}, Anda harus mengamatinya untuk usia 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 "cerdas" hilang. Putaran asinkron yang membuat saya merasa begitu puas adalah diganti dengan loop sementara yang membosankan, terpercaya. Jauh lebih baik. Di masa mendatang, Anda akan mendapatkan iterator asinkron, yang akan mengganti loop while dengan loop for-of, sehingga membuatnya lebih rapi.

Sintaksis fungsi asinkron lainnya

Saya telah menunjukkan async function() {} kepada Anda, tetapi kata kunci async dapat berupa digunakan dengan sintaks fungsi lainnya:

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 berurutan

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 1.000 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 memerlukan waktu 500 md untuk diselesaikan, karena kedua proses tunggu terjadi secara bersamaan. Mari kita lihat sebuah contoh praktis.

Contoh: menghasilkan output secara berurutan

Misalkan Anda ingin mengambil serangkaian URL dan mencatatnya sesegera mungkin, di bagian urutan yang benar.

napas dalam-dalam - berikut ini 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 smart. Tapi ini adalah coding yang sangat cerdas, yang akan lebih baik jika Anda tidak melakukannya.

Namun, ketika mengonversi di atas menjadi fungsi asinkron, Anda tergoda untuk terlalu sekuensial:

Tidak disarankan - terlalu berurutan
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 dibaca sepenuhnya, dan seterusnya. Ini jauh lebih lambat dari 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 "cerdas" Bit reduce 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 async polyfill.

Babel akan melakukannya untuk Anda, berikut contohnya melalui Babel REPL

  • perhatikan betapa miripnya kode yang ditranspilasi itu. Transformasi ini adalah bagian dari Preset es2017 Babel.

Saya sarankan pendekatan transpilasi, karena Anda bisa menonaktifkannya setelah browser target mendukung fungsi asinkron, tetapi jika Anda benar-benar tidak ingin menggunakan transpiler, Anda dapat mengambil Polis Babel dan menggunakannya sendiri. Jangan gunakan:

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

...Anda akan menyertakan polyfill dan tulis:

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, cara kerjanya sama.

Solusi: regenerator

Jika Anda menargetkan {i>browser<i} lama, Babel juga dapat mentranspilasi generator, memungkinkan Anda untuk menggunakan fungsi asinkron sampai ke IE8. Untuk melakukan hal ini, Anda memerlukan Preset es2017 Babel dan preset es2015.

Outputnya tidak terlalu bagus, jadi hati-hati dengan penggelembungan kode.

Sinkronkan semua hal.

Setelah fungsi asinkron ditempatkan di semua browser, gunakan fungsi tersebut setiap fungsi yang mengembalikan promise! Mereka tidak hanya membuat kode Anda lebih rapi, tetapi juga memastikan fungsi tersebut akan selalu menampilkan promise.

Saya sangat tertarik dengan fungsi asinkron kembali 2014, dan hal yang sangat bagus melihat mereka mendarat, secara nyata, dalam {i>browser<i}. Wah!