Mari kita lihat Async/Await di JavaScript menggunakan contoh

Penulis artikel membahas contoh Async/Await di JavaScript. Secara keseluruhan, Async/Await adalah cara mudah untuk menulis kode asinkron. Sebelum fitur ini muncul, kode tersebut ditulis menggunakan callback dan janji. Penulis artikel asli mengungkapkan kelebihan Async/Await dengan menganalisis berbagai contoh.

Kami mengingatkan: untuk semua pembaca "Habr" - diskon 10 rubel saat mendaftar di kursus Skillbox apa pun menggunakan kode promosi "Habr".

Skillbox merekomendasikan: Kursus online pendidikan "Pengembang Jawa".

Callback

Panggilan balik adalah fungsi yang panggilannya ditunda tanpa batas waktu. Sebelumnya, callback digunakan di area kode yang hasilnya tidak dapat diperoleh dengan segera.

Berikut ini contoh pembacaan file secara asinkron di Node.js:

fs.readFile(__filename, 'utf-8', (err, data) => {
  if (err) {
    throw err;
  }
  console.log(data);
});

Masalah muncul ketika Anda perlu melakukan beberapa operasi asinkron sekaligus. Bayangkan skenario ini: permintaan dibuat ke database pengguna Arfat, Anda perlu membaca kolom profile_img_url dan mendownload gambar dari server someserver.com.
Setelah didownload, kita convert gambarnya ke format lain, misalnya dari PNG ke JPEG. Jika konversi berhasil, surat dikirim ke email pengguna. Selanjutnya, informasi tentang acara tersebut dimasukkan ke dalam file transformasi.log, yang menunjukkan tanggal.

Perlu diperhatikan tumpang tindih callback dan banyaknya }) di bagian akhir kode. Ini disebut Callback Hell atau Pyramid of Doom.

Kerugian dari metode ini jelas:

  • Kode ini sulit dibaca.
  • Menangani kesalahan juga sulit, yang sering kali menyebabkan kualitas kode buruk.

Untuk mengatasi masalah ini, janji ditambahkan ke JavaScript. Mereka memungkinkan Anda mengganti callback yang bersarang dalam dengan kata .then.

Aspek positif dari janji adalah membuat kode lebih mudah dibaca, dari atas ke bawah daripada dari kiri ke kanan. Namun, janji juga mempunyai masalah:

  • Anda perlu menambahkan banyak .then.
  • Daripada mencoba/menangkap, .catch digunakan untuk menangani semua kesalahan.
  • Bekerja dengan banyak janji dalam satu loop tidak selalu nyaman; dalam beberapa kasus, hal itu memperumit kode.

Berikut ini adalah soal yang akan menunjukkan maksud dari poin terakhir.

Misalkan kita memiliki perulangan for yang mencetak urutan angka dari 0 hingga 10 pada interval acak (0–n detik). Dengan menggunakan janji, Anda perlu mengubah loop ini sehingga angka-angka tersebut dicetak secara berurutan dari 0 hingga 10. Jadi, jika diperlukan waktu 6 detik untuk mencetak angka nol dan 2 detik untuk mencetak angka satu, angka nol harus dicetak terlebih dahulu, lalu hitungan mundur untuk mencetaknya akan dimulai.

Dan tentu saja, kami tidak menggunakan Async/Await atau .sort untuk mengatasi masalah ini. Contoh solusinya ada di bagian akhir.

Fungsi asinkron

Penambahan fungsi async di ES2017 (ES8) menyederhanakan tugas bekerja dengan janji. Saya perhatikan bahwa fungsi async bekerja “di atas” janji. Fungsi-fungsi ini tidak mewakili konsep yang berbeda secara kualitatif. Fungsi async dimaksudkan sebagai alternatif kode yang menggunakan janji.

Async/Await memungkinkan pengorganisasian pekerjaan dengan kode asinkron dalam gaya sinkron.

Oleh karena itu, mengetahui janji memudahkan untuk memahami prinsip Async/Await.

sintaksis

Biasanya terdiri dari dua kata kunci: async dan menunggu. Kata pertama mengubah fungsinya menjadi asynchronous. Fungsi tersebut memungkinkan penggunaan menunggu. Jika tidak, penggunaan fungsi ini akan menghasilkan kesalahan.

// With function declaration
 
async function myFn() {
  // await ...
}
 
// With arrow function
 
const myFn = async () => {
  // await ...
}
 
function myFn() {
  // await fn(); (Syntax Error since no async)
}
 

Async disisipkan di awal deklarasi fungsi, dan dalam kasus fungsi panah, di antara tanda “=” dan tanda kurung.

Fungsi-fungsi ini dapat ditempatkan pada suatu objek sebagai metode atau digunakan dalam deklarasi kelas.

// As an object's method
 
const obj = {
  async getName() {
    return fetch('https://www.example.com');
  }
}
 
// In a class
 
class Obj {
  async getResource() {
    return fetch('https://www.example.com');
  }
}

Catatan! Perlu diingat bahwa konstruktor kelas dan pengambil/penyetel tidak boleh asinkron.

Semantik dan aturan eksekusi

Fungsi async pada dasarnya mirip dengan fungsi JS standar, tetapi ada pengecualian.

Jadi, fungsi async selalu memberikan janji:

async function fn() {
  return 'hello';
}
fn().then(console.log)
// hello

Secara khusus, fn mengembalikan string hello. Karena ini adalah fungsi asinkron, nilai string dibungkus dengan janji menggunakan konstruktor.

Berikut desain alternatif tanpa Async:

function fn() {
  return Promise.resolve('hello');
}
 
fn().then(console.log);
// hello

Dalam hal ini, janji dikembalikan “secara manual”. Fungsi asinkron selalu dibungkus dengan janji baru.

Jika nilai yang dikembalikan adalah primitif, fungsi async akan mengembalikan nilai tersebut dengan membungkusnya dalam sebuah janji. Jika nilai yang dikembalikan adalah objek janji, penyelesaiannya dikembalikan dalam janji baru.

const p = Promise.resolve('hello')
p instanceof Promise;
// true
 
Promise.resolve(p) === p;
// true
 

Namun apa yang terjadi jika ada kesalahan di dalam fungsi asinkron?

async function foo() {
  throw Error('bar');
}
 
foo().catch(console.log);

Jika tidak diproses, foo() akan mengembalikan janji dengan penolakan. Dalam situasi ini, Promise.reject yang berisi kesalahan akan dikembalikan, bukan Promise.resolve.

Fungsi async selalu menghasilkan janji, apa pun yang dikembalikan.

Fungsi asinkron dijeda setiap menunggu.

Tunggu mempengaruhi ekspresi. Jadi, jika ekspresi tersebut adalah sebuah janji, fungsi async akan ditangguhkan hingga janji tersebut dipenuhi. Jika ekspresi tersebut bukan janji, ekspresi tersebut diubah menjadi janji melalui Promise.resolve, lalu diselesaikan.

// utility function to cause delay
// and get random value
 
const delayAndGetRandom = (ms) => {
  return new Promise(resolve => setTimeout(
    () => {
      const val = Math.trunc(Math.random() * 100);
      resolve(val);
    }, ms
  ));
};
 
async function fn() {
  const a = await 9;
  const b = await delayAndGetRandom(1000);
  const c = await 5;
  await delayAndGetRandom(1000);
 
  return a + b * c;
}
 
// Execute fn
fn().then(console.log);

Dan berikut penjelasan cara kerja fungsi fn.

  • Setelah memanggilnya, baris pertama diubah dari const a = waiting 9; di const a = menunggu Promise.resolve(9);.
  • Setelah menggunakan Await, eksekusi fungsi ditangguhkan hingga a mendapatkan nilainya (dalam situasi saat ini adalah 9).
  • delayAndGetRandom(1000) menjeda eksekusi fungsi fn hingga selesai sendiri (setelah 1 detik). Ini secara efektif menghentikan fungsi fn selama 1 detik.
  • delayAndGetRandom(1000) melalui tekad mengembalikan nilai acak, yang kemudian ditetapkan ke variabel b.
  • Nah, kasus variabel c sama halnya dengan variabel a. Setelah itu, semuanya berhenti sejenak, tetapi sekarang delayAndGetRandom(1000) tidak mengembalikan apa pun karena tidak diperlukan.
  • Hasilnya, nilainya dihitung menggunakan rumus a + b * c. Hasilnya dibungkus dengan janji menggunakan Promise.resolve dan dikembalikan oleh fungsinya.

Jeda ini mungkin mengingatkan pada generator di ES6, tapi ada sesuatu di dalamnya alasanmu.

Memecahkan masalah

Nah, sekarang mari kita lihat solusi dari permasalahan di atas.

Fungsi finishMyTask menggunakan Await untuk menunggu hasil operasi seperti queryDatabase, sendEmail, logTaskInFile, dan lain-lain. Jika Anda membandingkan solusi ini dengan solusi yang menggunakan janji, persamaannya akan terlihat jelas. Namun, versi Async/Await sangat menyederhanakan semua kerumitan sintaksis. Dalam hal ini, tidak ada banyak panggilan balik dan rantai seperti .then/.catch.

Berikut solusi dengan output angka, ada dua pilihan.

const wait = (i, ms) => new Promise(resolve => setTimeout(() => resolve(i), ms));
 
// Implementation One (Using for-loop)
const printNumbers = () => new Promise((resolve) => {
  let pr = Promise.resolve(0);
  for (let i = 1; i <= 10; i += 1) {
    pr = pr.then((val) => {
      console.log(val);
      return wait(i, Math.random() * 1000);
    });
  }
  resolve(pr);
});
 
// Implementation Two (Using Recursion)
 
const printNumbersRecursive = () => {
  return Promise.resolve(0).then(function processNextPromise(i) {
 
    if (i === 10) {
      return undefined;
    }
 
    return wait(i, Math.random() * 1000).then((val) => {
      console.log(val);
      return processNextPromise(i + 1);
    });
  });
};

Dan berikut ini solusinya menggunakan fungsi async.

async function printNumbersUsingAsync() {
  for (let i = 0; i < 10; i++) {
    await wait(i, Math.random() * 1000);
    console.log(i);
  }
}

Menangani kesalahan

Kesalahan yang tidak tertangani dibungkus dengan janji yang ditolak. Namun, fungsi async dapat menggunakan try/catch untuk menangani kesalahan secara sinkron.

async function canRejectOrReturn() {
  // wait one second
  await new Promise(res => setTimeout(res, 1000));
 
// Reject with ~50% probability
  if (Math.random() > 0.5) {
    throw new Error('Sorry, number too big.')
  }
 
return 'perfect number';
}

canRejectOrReturn() adalah fungsi asinkron yang berhasil (“angka sempurna”) atau gagal dengan kesalahan (“Maaf, angka terlalu besar”).

async function foo() {
  try {
    await canRejectOrReturn();
  } catch (e) {
    return 'error caught';
  }
}

Karena contoh di atas mengharapkan canRejectOrReturn untuk dieksekusi, kegagalannya sendiri akan mengakibatkan eksekusi blok catch. Akibatnya, fungsi foo akan berakhir dengan tidak terdefinisi (ketika tidak ada yang dikembalikan di blok coba) atau dengan kesalahan yang tertangkap. Hasilnya, fungsi ini tidak akan gagal karena try/catch akan menangani fungsi foo itu sendiri.

Berikut contoh lain:

async function foo() {
  try {
    return canRejectOrReturn();
  } catch (e) {
    return 'error caught';
  }
}

Perlu diperhatikan fakta bahwa dalam contoh, canRejectOrReturn dikembalikan dari foo. Foo dalam hal ini diakhiri dengan angka sempurna atau mengembalikan Kesalahan (“Maaf, angka terlalu besar”). Blok tangkapan tidak akan pernah dieksekusi.

Masalahnya adalah foo mengembalikan janji yang diberikan dari canRejectOrReturn. Jadi solusi untuk foo menjadi solusi untuk canRejectOrReturn. Dalam hal ini, kode hanya terdiri dari dua baris:

try {
    const promise = canRejectOrReturn();
    return promise;
}

Inilah yang terjadi jika Anda menggunakan menunggu dan kembali bersama-sama:

async function foo() {
  try {
    return await canRejectOrReturn();
  } catch (e) {
    return 'error caught';
  }
}

Dalam kode di atas, foo akan berhasil keluar dengan angka sempurna dan kesalahan tertangkap. Tidak akan ada penolakan di sini. Tapi foo akan kembali dengan canRejectOrReturn, bukan dengan tidak terdefinisi. Mari kita pastikan hal ini dengan menghapus baris return waiting canRejectOrReturn() :

try {
    const value = await canRejectOrReturn();
    return value;
}
// …

Kesalahan dan jebakan umum

Dalam beberapa kasus, penggunaan Async/Await dapat menyebabkan kesalahan.

Lupa menunggu

Ini cukup sering terjadi - kata kunci menunggu dilupakan sebelum janji:

async function foo() {
  try {
    canRejectOrReturn();
  } catch (e) {
    return 'caught';
  }
}

Seperti yang Anda lihat, tidak ada menunggu atau kembali dalam kode. Oleh karena itu foo selalu keluar dengan undefinisi tanpa penundaan 1 detik. Namun janji itu akan dipenuhi. Jika terjadi kesalahan atau penolakan, maka UnhandledPromiseRejectionWarning akan dipanggil.

Fungsi Async dalam Panggilan Balik

Fungsi async cukup sering digunakan di .map atau .filter sebagai callback. Contohnya adalah fungsi FetchPublicReposCount(nama pengguna), yang mengembalikan jumlah repositori terbuka di GitHub. Katakanlah ada tiga pengguna yang metriknya kita perlukan. Berikut kode untuk tugas ini:

const url = 'https://api.github.com/users';
 
// Utility fn to fetch repo counts
const fetchPublicReposCount = async (username) => {
  const response = await fetch(`${url}/${username}`);
  const json = await response.json();
  return json['public_repos'];
}

Kami membutuhkan akun ArfatSalman, octocat, norvig. Dalam hal ini kami melakukan:

const users = [
  'ArfatSalman',
  'octocat',
  'norvig'
];
 
const counts = users.map(async username => {
  const count = await fetchPublicReposCount(username);
  return count;
});

Sebaiknya perhatikan Await di panggilan balik .map. Di sini yang dihitung adalah serangkaian janji, dan .map merupakan panggilan balik anonim untuk setiap pengguna tertentu.

Penggunaan menunggu yang terlalu konsisten

Mari kita ambil kode ini sebagai contoh:

async function fetchAllCounts(users) {
  const counts = [];
  for (let i = 0; i < users.length; i++) {
    const username = users[i];
    const count = await fetchPublicReposCount(username);
    counts.push(count);
  }
  return counts;
}

Disini nomor repo ditempatkan pada variabel count, kemudian nomor tersebut ditambahkan ke array counts. Masalah dengan kode ini adalah hingga data pengguna pertama tiba dari server, semua pengguna berikutnya akan berada dalam mode siaga. Jadi, hanya satu pengguna yang diproses dalam satu waktu.

Jika, misalnya, dibutuhkan sekitar 300 ms untuk memproses satu pengguna, maka untuk semua pengguna itu sudah satu detik; waktu yang dihabiskan secara linear bergantung pada jumlah pengguna. Namun karena perolehan jumlah repo tidak bergantung satu sama lain, prosesnya dapat diparalelkan. Ini memerlukan kerja dengan .map dan Promise.all:

async function fetchAllCounts(users) {
  const promises = users.map(async username => {
    const count = await fetchPublicReposCount(username);
    return count;
  });
  return Promise.all(promises);
}

Promise.all menerima serangkaian janji sebagai masukan dan mengembalikan janji. Yang terakhir, setelah semua janji dalam susunan telah selesai atau pada penolakan pertama, selesai. Mungkin saja semuanya tidak dimulai pada waktu yang sama - untuk memastikan start simultan, Anda dapat menggunakan p-map.

Kesimpulan

Fungsi async menjadi semakin penting untuk pengembangan. Nah, untuk penggunaan fungsi async yang adaptif, Anda harus menggunakan Iterator Asinkron. Pengembang JavaScript harus berpengalaman dalam hal ini.

Skillbox merekomendasikan:

Sumber: www.habr.com

Tambah komentar