Mari lihat Async/Await dalam JavaScript menggunakan contoh

Pengarang artikel mengkaji contoh Async/Await dalam JavaScript. Secara keseluruhan, Async/Await ialah cara yang mudah untuk menulis kod tak segerak. Sebelum ciri ini muncul, kod tersebut telah ditulis menggunakan panggilan balik dan janji. Penulis artikel asal mendedahkan kelebihan Async/Await dengan menganalisis pelbagai contoh.

Kami mengingatkan: untuk semua pembaca "Habr" - diskaun sebanyak 10 rubel apabila mendaftar dalam mana-mana kursus Skillbox menggunakan kod promosi "Habr".

Skillbox mengesyorkan: Kursus dalam talian pendidikan "Pembangun Java".

Panggil balik

Panggilan balik ialah fungsi yang panggilannya ditangguhkan selama-lamanya. Sebelum ini, panggilan balik telah digunakan dalam kawasan kod yang hasilnya tidak boleh diperolehi dengan segera.

Berikut ialah contoh membaca fail secara tak segerak dalam Node.js:

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

Masalah timbul apabila anda perlu melakukan beberapa operasi tak segerak serentak. Mari bayangkan senario ini: permintaan dibuat kepada pangkalan data pengguna Arfat, anda perlu membaca medan profile_img_urlnya dan memuat turun imej dari pelayan someserver.com.
Selepas memuat turun, kami menukar imej kepada format lain, contohnya daripada PNG kepada JPEG. Jika penukaran berjaya, surat dihantar ke e-mel pengguna. Seterusnya, maklumat tentang acara tersebut dimasukkan ke dalam fail transformations.log, yang menunjukkan tarikh.

Perlu diberi perhatian kepada pertindihan panggilan balik dan bilangan besar }) di bahagian akhir kod. Ia dipanggil Callback Hell atau Pyramid of Doom.

Kelemahan kaedah ini jelas:

  • Kod ini sukar dibaca.
  • Ia juga sukar untuk mengendalikan ralat, yang sering membawa kepada kualiti kod yang lemah.

Untuk menyelesaikan masalah ini, janji telah ditambahkan pada JavaScript. Mereka membenarkan anda menggantikan sarang panggilan balik dalam dengan perkataan .then.

Aspek positif janji ialah mereka menjadikan kod itu lebih mudah dibaca, dari atas ke bawah dan bukannya dari kiri ke kanan. Walau bagaimanapun, janji juga mempunyai masalahnya:

  • Anda perlu menambah banyak .kemudian.
  • Daripada cuba/tangkap, .catch digunakan untuk mengendalikan semua ralat.
  • Bekerja dengan berbilang janji dalam satu gelung tidak selalunya mudah; dalam beberapa kes, ia merumitkan kod.

Berikut adalah masalah yang akan menunjukkan maksud titik terakhir.

Katakan kita mempunyai gelung for yang mencetak urutan nombor dari 0 hingga 10 pada selang rawak (0–n saat). Menggunakan janji, anda perlu menukar gelung ini supaya nombor dicetak dalam urutan dari 0 hingga 10. Jadi, jika mengambil masa 6 saat untuk mencetak sifar dan 2 saat untuk mencetak satu, sifar harus dicetak terlebih dahulu, dan kemudian undur untuk mencetak satu akan bermula.

Dan sudah tentu, kami tidak menggunakan Async/Await atau .sort untuk menyelesaikan masalah ini. Contoh penyelesaian adalah di penghujung.

Fungsi Async

Penambahan fungsi async dalam ES2017 (ES8) memudahkan tugas bekerja dengan janji. Saya perhatikan bahawa fungsi async berfungsi "di atas" janji. Fungsi ini tidak mewakili konsep yang berbeza secara kualitatif. Fungsi Async bertujuan sebagai alternatif kepada kod yang menggunakan janji.

Async/Await memungkinkan untuk mengatur kerja dengan kod tak segerak dalam gaya segerak.

Oleh itu, mengetahui janji menjadikannya lebih mudah untuk memahami prinsip Async/Await.

sintaks

Biasanya ia terdiri daripada dua kata kunci: async dan tunggu. Perkataan pertama menukar fungsi menjadi tak segerak. Fungsi sedemikian membolehkan penggunaan menunggu. Dalam mana-mana kes lain, menggunakan fungsi ini akan menghasilkan ralat.

// 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 pada awal pengisytiharan fungsi, dan dalam kes fungsi anak panah, antara tanda "=" dan kurungan.

Fungsi ini boleh diletakkan dalam objek sebagai kaedah atau digunakan dalam pengisytiharan 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');
  }
}

NB! Perlu diingat bahawa pembina kelas dan getter/setter tidak boleh tidak segerak.

Semantik dan peraturan pelaksanaan

Fungsi Async pada asasnya serupa dengan fungsi JS standard, tetapi terdapat pengecualian.

Oleh itu, fungsi async sentiasa mengembalikan janji:

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

Khususnya, fn mengembalikan rentetan helo. Nah, kerana ini adalah fungsi tak segerak, nilai rentetan dibalut dengan janji menggunakan pembina.

Berikut ialah reka bentuk alternatif tanpa Async:

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

Dalam kes ini, janji dikembalikan "secara manual". Fungsi tak segerak sentiasa dibalut dengan janji baharu.

Jika nilai pulangan adalah primitif, fungsi async mengembalikan nilai dengan membungkusnya dalam janji. Jika nilai pulangan ialah objek janji, resolusinya dikembalikan dalam janji baharu.

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

Tetapi apa yang berlaku jika terdapat ralat dalam fungsi tak segerak?

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

Jika ia tidak diproses, foo() akan mengembalikan janji dengan penolakan. Dalam situasi ini, Promise.reject yang mengandungi ralat akan dikembalikan dan bukannya Promise.resolve.

Fungsi Async sentiasa mengeluarkan janji, tanpa mengira apa yang dikembalikan.

Fungsi tak segerak dijeda pada setiap penantian.

Menunggu mempengaruhi ekspresi. Jadi, jika ungkapan itu janji, fungsi async digantung sehingga janji itu dipenuhi. Jika ungkapan itu bukan janji, ia ditukar kepada janji melalui Promise.resolve dan kemudian 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 ialah penerangan tentang cara fungsi fn berfungsi.

  • Selepas memanggilnya, baris pertama ditukar daripada const a = tunggu 9; dalam const a = tunggu Promise.resolve(9);.
  • Selepas menggunakan Await, pelaksanaan fungsi digantung sehingga a mendapat nilainya (dalam keadaan semasa ialah 9).
  • delayAndGetRandom(1000) menjeda pelaksanaan fungsi fn sehingga ia selesai sendiri (selepas 1 saat). Ini secara berkesan menghentikan fungsi fn selama 1 saat.
  • delayAndGetRandom(1000) melalui menyelesaikan mengembalikan nilai rawak, yang kemudiannya diberikan kepada pembolehubah b.
  • Nah, kes dengan pembolehubah c adalah serupa dengan kes dengan pembolehubah a. Selepas itu, semuanya berhenti seketika, tetapi kini delayAndGetRandom(1000) tidak mengembalikan apa-apa kerana ia tidak diperlukan.
  • Akibatnya, nilai dikira menggunakan formula a + b * c. Hasilnya dibalut dengan janji menggunakan Promise.resolve dan dikembalikan oleh fungsi.

Jeda ini mungkin mengingatkan penjana dalam ES6, tetapi ada sesuatu untuknya alasan anda.

Menyelesaikan masalah

Nah, sekarang mari kita lihat penyelesaian kepada masalah yang disebutkan di atas.

Fungsi finishMyTask menggunakan Await untuk menunggu hasil operasi seperti queryDatabase, sendEmail, logTaskInFile dan lain-lain. Jika anda membandingkan penyelesaian ini dengan penyelesaian di mana janji digunakan, persamaan akan menjadi jelas. Walau bagaimanapun, versi Async/Await sangat memudahkan semua kerumitan sintaksis. Dalam kes ini, tiada bilangan panggilan balik dan rantai yang besar seperti .then/.catch.

Berikut adalah penyelesaian dengan output nombor, terdapat 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 inilah penyelesaian menggunakan fungsi async.

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

Ralat pemprosesan

Kesilapan yang tidak terurus dibaluti dengan janji yang ditolak. Walau bagaimanapun, fungsi async boleh menggunakan try/catch untuk mengendalikan ralat secara serentak.

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() ialah fungsi tak segerak yang sama ada berjaya (β€œnombor sempurna”) atau gagal dengan ralat (β€œMaaf, nombor terlalu besar”).

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

Oleh kerana contoh di atas menjangkakan canRejectOrReturn akan dilaksanakan, kegagalannya sendiri akan mengakibatkan pelaksanaan blok tangkapan. Akibatnya, fungsi foo akan berakhir dengan sama ada tidak ditentukan (apabila tiada apa yang dikembalikan dalam blok cuba) atau dengan ralat yang ditangkap. Akibatnya, fungsi ini tidak akan gagal kerana try/catch akan mengendalikan fungsi foo itu sendiri.

Berikut adalah contoh lain:

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

Perlu diberi perhatian kepada fakta bahawa dalam contoh, canRejectOrReturn dikembalikan daripada foo. Foo dalam kes ini sama ada ditamatkan dengan nombor yang sempurna atau mengembalikan Ralat ("Maaf, nombor terlalu besar"). Blok tangkapan tidak akan dilaksanakan.

Masalahnya ialah foo mengembalikan janji yang diluluskan daripada canRejectOrReturn. Jadi penyelesaian kepada foo menjadi penyelesaian kepada canRejectOrReturn. Dalam kes ini, kod akan terdiri daripada dua baris sahaja:

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

Inilah yang berlaku jika anda menggunakan await dan return bersama-sama:

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

Dalam kod di atas, foo akan berjaya keluar dengan kedua-dua nombor sempurna dan ralat ditangkap. Tidak akan ada penolakan di sini. Tetapi foo akan kembali dengan canRejectOrReturn, bukan dengan undefined. Mari kita pastikan perkara ini dengan mengalih keluar barisan return await canRejectOrReturn():

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

Kesilapan dan perangkap biasa

Dalam sesetengah kes, menggunakan Async/Await boleh membawa kepada ralat.

Terlupa menunggu

Ini berlaku agak kerap - kata kunci tunggu dilupakan sebelum janji:

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

Seperti yang anda lihat, tiada menunggu atau kembali dalam kod. Oleh itu foo sentiasa keluar dengan undefined tanpa kelewatan 1 saat. Tetapi janji itu akan ditunaikan. Jika ia menimbulkan ralat atau penolakan, maka UnhandledPromiseRejectionWarning akan dipanggil.

Fungsi Async dalam Panggilan Balik

Fungsi async agak kerap digunakan dalam .map atau .filter sebagai panggilan balik. Contohnya ialah fungsi fetchPublicReposCount(nama pengguna), yang mengembalikan bilangan repositori terbuka pada GitHub. Katakan terdapat tiga pengguna yang metriknya kita perlukan. Berikut ialah kod untuk tugasan 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 memerlukan akaun ArfatSalman, octocat, norvig. Dalam kes ini kita lakukan:

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

Perlu diberi perhatian pada Await dalam panggilan balik .map. Di sini dikira ialah pelbagai janji dan .map ialah panggilan balik tanpa nama untuk setiap pengguna yang ditentukan.

Penggunaan menunggu yang terlalu konsisten

Mari kita ambil kod 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;
}

Di sini nombor repo diletakkan dalam pembolehubah kiraan, kemudian nombor ini ditambah pada tatasusunan kiraan. Masalah dengan kod tersebut ialah sehingga data pengguna pertama tiba dari pelayan, semua pengguna berikutnya akan berada dalam mod siap sedia. Oleh itu, hanya seorang pengguna diproses pada satu masa.

Jika, sebagai contoh, ia mengambil masa kira-kira 300 ms untuk memproses satu pengguna, maka untuk semua pengguna ia sudah menjadi satu saat; masa yang dibelanjakan secara linear bergantung pada bilangan pengguna. Tetapi kerana mendapatkan bilangan repo tidak bergantung antara satu sama lain, proses boleh diselaraskan. Ini memerlukan bekerja 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 pelbagai janji sebagai input dan mengembalikan janji. Yang terakhir, selepas semua janji dalam tatasusunan telah selesai atau pada penolakan pertama, selesai. Ia mungkin berlaku bahawa mereka semua tidak bermula pada masa yang sama - untuk memastikan permulaan serentak, anda boleh menggunakan p-map.

Kesimpulan

Fungsi Async menjadi semakin penting untuk pembangunan. Nah, untuk penggunaan adaptif fungsi async, anda harus gunakan Async Iterators. Pembangun JavaScript harus mahir dalam hal ini.

Skillbox mengesyorkan:

Sumber: www.habr.com

Tambah komen