Jika Anda baru memulai pemrograman, kemungkinan besar, Anda menganggap program sebagai sekumpulan blok logika yang berurutan, di mana setiap blok melakukan hal tertentu dan meneruskan hasilnya sehingga blok berikutnya dapat berjalan dan seterusnya, dan untuk sebagian besar Anda benar, sebagian besar program berjalan secara berurutan, model ini memungkinkan kita membuat program yang mudah untuk ditulis dan dipelihara. Namun ada kasus penggunaan tertentu di mana model sekuensial ini tidak berfungsi, atau tidak optimal. Sebagai contoh, perhatikan aplikasi pembaca buku. Aplikasi ini memiliki beberapa fitur lanjutan seperti menemukan semua kemunculan sebuah kata, menavigasi antar bookmark, dan sejenisnya. Sekarang bayangkan pengguna sedang membaca buku panjang dan memutuskan untuk mencari semua kemunculan kata umum seperti “The”. Aplikasi biasanya memerlukan waktu beberapa detik untuk menemukan dan mengindeks semua kemunculan kata tersebut. Dalam program sekuensial, pengguna tidak dapat berinteraksi dengan aplikasi (mengubah halaman atau menyorot teks) hingga operasi pencarian terpenuhi. Mudah-mudahan, Anda dapat melihat bahwa itu bukanlah pengalaman pengguna yang optimal!
Diagram mengilustrasikan alur eksekusi khas aplikasi pembaca buku. Jika pengguna memulai operasi yang berjalan lama (dalam hal ini pencarian semua kemunculan "the" dalam buku besar), aplikasi "membeku" selama durasi operasi tersebut. Dalam hal ini, pengguna akan terus mengklik tombol bookmark berikutnya tanpa hasil hingga operasi pencarian selesai dan semua operasi akan diterapkan sekaligus sehingga memberikan pengguna akhir perasaan aplikasi yang lambat.
Anda mungkin telah memperhatikan bahwa contoh ini tidak sesuai dengan model sekuensial yang kami perkenalkan sebelumnya. Hal ini karena operasi di sini independen satu sama lain. Pengguna tidak perlu mengetahui jumlah kemunculan "the" untuk menavigasi ke bookmark berikutnya, sehingga urutan pelaksanaan operasi tidak terlalu penting. Kita tidak perlu menunggu akhir dari operasi pencarian sebelum kita dapat menavigasi ke bookmark berikutnya. Kemungkinan peningkatan pada alur eksekusi sebelumnya didasarkan pada logika ini: kita dapat menjalankan operasi pencarian panjang di latar belakang, melanjutkan operasi masuk apa pun, dan setelah operasi panjang selesai, kita cukup memberi tahu pengguna. Alur eksekusinya menjadi sebagai berikut:
Dengan alur eksekusi ini, pengalaman pengguna meningkat secara signifikan. Sekarang pengguna dapat memulai operasi yang berjalan lama, melanjutkan untuk menggunakan aplikasi secara normal, dan mendapatkan pemberitahuan setelah operasi selesai. Ini adalah dasar dari pemrograman asinkron.
Javascript, di antara bahasa-bahasa lain, mendukung gaya pemrograman asinkron ini dengan menyediakan API ekstensif untuk mencapai hampir semua perilaku asinkron yang dapat Anda pikirkan. Pada akhirnya, Javascript pada dasarnya adalah bahasa asinkron. Jika kita mengacu pada contoh sebelumnya, logika asinkron adalah dasar dari semua aplikasi interaksi pengguna, dan Javascript terutama dibuat untuk digunakan pada browser di mana sebagian besar programnya merespons tindakan pengguna.
Berikut ini akan memberi Anda panduan singkat tentang Javascript Asinkron:
Panggilan balik
Dalam suatu program pada umumnya, Anda biasanya akan menemukan sejumlah fungsi. Untuk menggunakan suatu fungsi, kita memanggilnya dengan sekumpulan parameter. Kode fungsi akan mengeksekusi dan mengembalikan hasil, tidak ada yang luar biasa. Pemrograman asinkron sedikit menggeser logika ini. Kembali ke contoh aplikasi pembaca buku, kita tidak dapat menggunakan fungsi biasa untuk mengimplementasikan logika operasi pencarian karena operasi tersebut memerlukan waktu yang tidak diketahui. Fungsi reguler pada dasarnya akan kembali sebelum operasi selesai, dan ini bukan perilaku yang kita harapkan. Solusinya adalah dengan menentukan fungsi lain yang akan dijalankan setelah operasi pencarian selesai. Ini memodelkan kasus penggunaan kami karena program kami dapat melanjutkan alirannya secara normal dan setelah operasi pencarian selesai, fungsi yang ditentukan akan dijalankan untuk memberi tahu pengguna tentang hasil pencarian. Fungsi inilah yang kita sebut fungsi panggilan balik:
// Search occurrences function
function searchOccurrences(word, callback) {
try {
// search operation logic, result is in result variable
//....
callback(null, word, result);
} catch (err) {
callback(err);
}
}
// Search occurrences callback function
function handleSearchOccurrencesResult(err, word, result) {
if (err) {
console.log(`Search operation for ${word} ended with an error`);
} else console.log(`Search results for ${word}: ${result}`);
return;
}
searchOccurrences("the", handleSearchOccurrencesResult);
Pertama, kita mendefinisikan fungsi operasi pencarian, searchOccurrences. Dibutuhkan kata yang akan dicari dan parameter kedua "panggilan balik" yang akan berfungsi untuk dijalankan setelah operasi pencarian selesai. Fungsi operasi pencarian sengaja dibuat abstrak, kita hanya perlu fokus pada dua kemungkinan hasil: kasus pertama adalah ketika semuanya berjalan sukses dan kita memiliki hasil pencarian dalam variabel hasil. Dalam hal ini, kita hanya perlu memanggil fungsi callback dengan parameter berikut: parameter pertama adalah null artinya tidak ada kesalahan yang terjadi, parameter kedua adalah kata yang dicari, dan parameter ketiga dan mungkin yang paling penting dari ketiganya., adalah hasil dari operasi pencarian.
Kasus kedua adalah dimana terjadi error, ini juga merupakan kasus dimana eksekusi operasi pencarian selesai dan kita harus memanggil fungsi callback. Kami menggunakan blok coba dan tangkap untuk mencegat kesalahan apa pun dan kami hanya memanggil fungsi panggilan balik dengan objek kesalahan dari blok tangkapan.
Kami kemudian mendefinisikan fungsi panggilan balik, handleSearchOccurrences, kami menjaga logikanya cukup sederhana. Ini hanya masalah mencetak pesan ke konsol. Kami pertama-tama memeriksa parameter “err” untuk melihat apakah ada kesalahan yang terjadi pada fungsi utama. Dalam hal ini, kami hanya memberi tahu pengguna bahwa operasi pencarian berakhir dengan kesalahan. Jika tidak ada kesalahan yang muncul, kami mencetak pesan dengan hasil operasi pencarian.
Terakhir, kita memanggil fungsi searchOccurrences dengan kata “the”. Fungsi tersebut sekarang akan berjalan normal tanpa memblokir program utama dan setelah pencarian selesai, panggilan balik akan dijalankan dan kita akan mendapatkan pesan hasil baik dengan hasil pencarian atau pesan kesalahan.
Penting untuk disebutkan di sini bahwa kita hanya memiliki akses ke variabel hasil di dalam fungsi main dan panggilan balik. Jika kita mencoba sesuatu seperti ini:
let result;
function searchOccurrences(word, callback) {
try {
// search operation logic, result is in searchResult variable
//....
result = searchResult;
callback(null, word, result);
} catch (err) {
callback(err);
}
}
searchOccurrences("the", handleSearchOccurrencesResult);
console.log(result);
hasil pencetakan tidak akan ditentukan karena program tidak menunggu fungsi searchOccurrences dijalankan. Ini berpindah ke instruksi berikutnya yang merupakan pernyataan print sebelum variabel hasil ditugaskan di dalam fungsi utama. Hasilnya, kami akan mencetak variabel hasil yang belum ditetapkan.
Jadi berdasarkan logika ini, kita harus menyimpan semua kode yang menggunakan variabel hasil di dalam fungsi panggilan balik. Saat ini hal ini mungkin tidak tampak sebagai masalah, namun dapat dengan cepat menjadi masalah nyata. Bayangkan kasus di mana kita memiliki rangkaian fungsi asinkron yang perlu dijalankan secara berurutan. Dalam logika panggilan balik pada umumnya, Anda akan mengimplementasikan sesuatu seperti ini:
functionA(function (err, resA) {
///......
functionB(resA, function (err, resB) {
///......
functionC(resB, function (err, resC) {
///......
functionD(resC, function (err, resD) {
///......
});
});
});
});
Ingatlah bahwa setiap panggilan balik memiliki parameter kesalahan dan setiap kesalahan harus ditangani secara terpisah. Hal ini membuat kode yang sudah rumit di atas menjadi lebih rumit dan sulit dipelihara. Mudah-mudahan Promises hadir untuk mengatasi masalah callback hell, selanjutnya akan kita bahas.
Janji
Janji dibangun di atas panggilan balik dan beroperasi dengan cara yang sama. Mereka diperkenalkan sebagai bagian dari fitur ES6 untuk memecahkan beberapa masalah mencolok dengan callback seperti callback hell. Janji menyediakan fungsinya sendiri yang berjalan saat penyelesaian berhasil (resolve), dan ketika terjadi kesalahan (reject). Berikut ini menampilkan contoh searchOccurrences yang diimplementasikan dengan janji:
// Search occurrences function
function searchOccurrences(word) {
return new Promise((resolve, reject) => {
try {
// search operation logic, result is in result variable
//....
resolve(word, result);
} catch (err) {
reject(err);
}
});
}
searchOccurrences("the")
.then((word, result) => {
console.log(`Search results for ${word}: ${result}`);
})
.catch((err) => {
console.log(`Search operation ended with an error`);
});
Mari kita lihat perubahan yang kita terapkan:
Fungsi searchOccurrences mengembalikan sebuah janji. Di dalam janji, kami menjaga logika yang sama: kami memiliki dua fungsi penyelesaian dan penolakan yang mewakili panggilan balik kami daripada memiliki satu fungsi panggilan balik yang menangani eksekusi yang berhasil dan eksekusi dengan kesalahan. Janji memisahkan kedua hasil dan memberikan sintaksis yang bersih saat memanggil fungsi utama. Fungsi tekad “terkait” ke fungsi utama menggunakan kata kunci “lalu”. Di sini kita tinggal menentukan dua parameter fungsi resolusi dan mencetak hasil pencarian. Hal serupa juga terjadi pada fungsi reject, dapat di hook menggunakan kata kunci “catch”. Mudah-mudahan, Anda dapat menghargai keuntungan yang ditawarkan janji dalam hal keterbacaan dan kebersihan kode. Jika Anda masih memperdebatkannya, lihat bagaimana kita dapat menyelesaikan masalah callback hell dengan menyatukan fungsi-fungsi asinkron untuk dijalankan satu demi satu:
searchOccurrences("the")
.then(searchOccurrences("asynchronous"))
.then(searchOccurrences("javascript"))
.then(searchOccurrences("guide"))
.catch((err) => {
console.log(`Search operation ended with an error`);
});
Asinkron/Tunggu
Async/Await adalah tambahan terbaru pada toolbelt asinkron kami dalam Javascript. Diperkenalkan dengan ES8, mereka menyediakan lapisan abstraksi baru di atas fungsi asinkron hanya dengan “menunggu” eksekusi operasi asinkron. Alur blok program pada instruksi tersebut hingga diperoleh hasil dari operasi asinkron dan kemudian program akan melanjutkan ke instruksi berikutnya. Jika Anda memikirkan alur eksekusi sinkron, Anda benar. Kita telah sampai pada lingkaran penuh! Async/await berupaya menghadirkan kesederhanaan pemrograman sinkron ke dunia asinkron. Harap diingat bahwa ini hanya terlihat dalam eksekusi dan kode program. Semuanya tetap sama, Async/await masih menggunakan janji dan panggilan balik adalah elemen penyusunnya.
Mari kita lihat contoh kita dan mengimplementasikannya menggunakan Async/tunggu:
async function searchOccurrences(word) {
try {
// search operation logic, result is in result variable
//....
return result;
} catch (err) {
console.log(`Search operation for ${word} ended with an error`);
}
}
const word = "the";
const result = await searchOccurrences(word, handleSearchOccurrencesResult);
console.log(`Search results for ${word}: ${result}`);
Kode kita tidak banyak berubah, hal penting yang perlu diperhatikan di sini adalah kata kunci “async” sebelum deklarasi fungsi searchOccurrences. Ini menunjukkan bahwa fungsinya tidak sinkron. Juga, perhatikan kata kunci “menunggu” saat memanggil fungsi searchOccurrences. Ini akan menginstruksikan program untuk menunggu eksekusi fungsi hingga hasilnya dikembalikan sebelum program dapat melanjutkan ke instruksi berikutnya, dengan kata lain, variabel hasil akan selalu menyimpan nilai yang dikembalikan dari fungsi searchOccurrences dan bukan janji dari fungsinya, dalam hal ini, Async/Await tidak memiliki status tertunda sebagai Janji. Setelah eksekusi selesai, kita berpindah ke pernyataan print dan kali ini hasilnya sebenarnya berisi hasil operasi pencarian. Seperti yang diharapkan, kode baru memiliki perilaku yang sama seperti jika disinkronkan.
Hal kecil lainnya yang perlu diingat adalah karena kita tidak lagi memiliki fungsi panggilan balik, kita perlu menangani kesalahan searchOccurrences di dalam fungsi yang sama karena kita tidak bisa begitu saja menyebarkan kesalahan ke fungsi panggilan balik dan menanganinya di sana. Di sini kami hanya mencetak pesan kesalahan jika terjadi kesalahan sebagai contoh.
Penutupan
Pada artikel ini kita membahas berbagai pendekatan yang digunakan untuk mengimplementasikan logika asynchronous dalam Javascript. Kami memulai dengan mengeksplorasi contoh nyata mengapa kami perlu beralih dari gaya pemrograman sinkron biasa ke model asinkron. Kami kemudian beralih ke callback, yang merupakan blok penyusun utama Javascript asinkron. Keterbatasan callback mengarahkan kami pada berbagai alternatif yang ditambahkan selama bertahun-tahun untuk mengatasi keterbatasan ini, terutama janji dan Async/menunggu. Logika asinkron dapat ditemukan di mana saja di web, baik Anda memanggil API eksternal, memulai kueri database, menulis ke sistem file lokal, atau bahkan menunggu masukan pengguna pada formulir login. Mudah-mudahan, Anda sekarang merasa lebih percaya diri untuk mengatasi masalah ini dengan menulis Javascript Asinkron yang bersih dan mudah dipelihara!
Jika Anda menyukai artikel ini, silakan kunjungi Blog CLA tempat kami membahas berbagai topik tentang cara masuk ke dunia teknologi. Kunjungi juga saluran youtube kami untuk mengetahui workshop gratis kami sebelumnya dan ikuti kami di media sosial agar Anda tidak ketinggalan acara mendatang!