Kami menggunakan metode browser dalam contoh di sini
Untuk mendemonstrasikan penggunaan callback, janji, dan konsep abstrak lainnya, kami akan menggunakan beberapa metode browser: khususnya, memuat skrip dan melakukan manipulasi dokumen sederhana.
Jika Anda belum familiar dengan metode ini, dan penggunaannya dalam contoh membingungkan, Anda mungkin ingin membaca beberapa bab dari bagian tutorial selanjutnya.
Meskipun demikian, kami akan mencoba memperjelasnya. Tidak akan ada sesuatu yang rumit dari segi browser.
Banyak fungsi yang disediakan oleh lingkungan host JavaScript yang memungkinkan Anda menjadwalkan tindakan asinkron . Dengan kata lain, tindakan yang kita mulai sekarang, namun selesai kemudian.
Misalnya, salah satu fungsi tersebut adalah fungsi setTimeout
.
Ada contoh tindakan asinkron di dunia nyata lainnya, misalnya memuat skrip dan modul (kita akan membahasnya di bab selanjutnya).
Lihatlah fungsi loadScript(src)
, yang memuat skrip dengan src
:
fungsi loadScript(src) { // membuat tag <script> dan menambahkannya ke halaman // ini menyebabkan skrip dengan src yang diberikan mulai memuat dan dijalankan setelah selesai biarkan skrip = document.createElement('script'); skrip.src = src; document.head.append(skrip); }
Itu memasukkan tag <script src="…">
baru yang dibuat secara dinamis ke dalam dokumen dengan src
yang diberikan. Browser secara otomatis mulai memuatnya dan mengeksekusinya setelah selesai.
Kita dapat menggunakan fungsi ini seperti ini:
// memuat dan mengeksekusi skrip pada jalur yang ditentukan loadScript('/saya/script.js');
Skrip dijalankan “secara asinkron”, saat mulai memuat sekarang, tetapi dijalankan kemudian, ketika fungsinya telah selesai.
Jika ada kode di bawah loadScript(…)
, kode tersebut tidak akan menunggu hingga pemuatan skrip selesai.
loadScript('/saya/script.js'); // kode di bawah loadScript // tidak menunggu pemuatan skrip selesai // ...
Katakanlah kita perlu menggunakan skrip baru segera setelah skrip tersebut dimuat. Ini mendeklarasikan fungsi baru, dan kami ingin menjalankannya.
Namun jika kita melakukannya segera setelah panggilan loadScript(…)
, itu tidak akan berhasil:
loadScript('/saya/script.js'); // skrip memiliki "fungsi newFunction() {…}" fungsi baru(); // tidak ada fungsi seperti itu!
Tentu saja, browser mungkin tidak punya waktu untuk memuat skrip. Sampai saat ini, fungsi loadScript
tidak menyediakan cara untuk melacak penyelesaian pemuatan. Script dimuat dan akhirnya dijalankan, itu saja. Tapi kami ingin tahu kapan hal itu terjadi, untuk menggunakan fungsi dan variabel baru dari skrip itu.
Mari tambahkan fungsi callback
sebagai argumen kedua pada loadScript
yang harus dijalankan saat skrip dimuat:
fungsi loadScript(src, panggilan balik) { biarkan skrip = document.createElement('script'); skrip.src = src; script.onload = () => panggilan balik(skrip); document.head.append(skrip); }
Peristiwa onload
dijelaskan dalam artikel Pemuatan sumber daya: onload dan onerror, pada dasarnya menjalankan fungsi setelah skrip dimuat dan dijalankan.
Sekarang jika kita ingin memanggil fungsi baru dari skrip, kita harus menuliskannya di callback:
loadScript('/my/script.js', function() { // panggilan balik dijalankan setelah skrip dimuat fungsi baru(); // jadi sekarang berhasil ... });
Itulah idenya: argumen kedua adalah fungsi (biasanya anonim) yang berjalan ketika tindakan selesai.
Berikut ini contoh yang dapat dijalankan dengan skrip nyata:
fungsi loadScript(src, panggilan balik) { biarkan skrip = document.createElement('script'); skrip.src = src; script.onload = () => panggilan balik(skrip); document.head.append(skrip); } loadScript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js', skrip => { alert(`Keren, skrip ${script.src} dimuat`); peringatan( _ ); // _ adalah fungsi yang dideklarasikan dalam skrip yang dimuat });
Itu disebut gaya pemrograman asinkron “berbasis panggilan balik”. Fungsi yang melakukan sesuatu secara asinkron harus menyediakan argumen callback
tempat kita menjalankan fungsi tersebut setelah selesai.
Di sini kami melakukannya dalam loadScript
, tetapi tentu saja ini merupakan pendekatan umum.
Bagaimana kita bisa memuat dua skrip secara berurutan: yang pertama, dan yang kedua setelahnya?
Solusi alaminya adalah dengan memasukkan panggilan loadScript
kedua ke dalam panggilan balik, seperti ini:
loadScript('/my/script.js', function(skrip) { alert(`Keren, ${script.src} sudah dimuat, ayo muat satu lagi`); loadScript('/my/script2.js', function(skrip) { alert(`Keren, skrip kedua dimuat`); }); });
Setelah loadScript
bagian luar selesai, callback memulai loadScript bagian dalam.
Bagaimana jika kita menginginkan satu skrip lagi…?
loadScript('/my/script.js', function(skrip) { loadScript('/my/script2.js', function(skrip) { loadScript('/my/script3.js', function(skrip) { // ...lanjutkan setelah semua skrip dimuat }); }); });
Jadi, setiap tindakan baru ada di dalam panggilan balik. Itu bagus untuk beberapa tindakan, tetapi tidak bagus untuk banyak tindakan, jadi kita akan segera melihat varian lainnya.
Dalam contoh di atas kami tidak mempertimbangkan kesalahan. Bagaimana jika pemuatan skrip gagal? Panggilan balik kami harus dapat bereaksi terhadap hal itu.
Berikut adalah versi loadScript
yang ditingkatkan yang melacak kesalahan pemuatan:
fungsi loadScript(src, panggilan balik) { biarkan skrip = document.createElement('script'); skrip.src = src; script.onload = () => panggilan balik(null, skrip); script.onerror = () => callback(Kesalahan baru(`Kesalahan pemuatan skrip untuk ${src}`)); document.head.append(skrip); }
Itu memanggil callback(null, script)
untuk pemuatan yang berhasil dan callback(error)
sebaliknya.
Penggunaan:
loadScript('/my/script.js', function(kesalahan, skrip) { jika (kesalahan) { // menangani kesalahan } kalau tidak { // skrip berhasil dimuat } });
Sekali lagi, resep yang kami gunakan untuk loadScript
sebenarnya cukup umum. Ini disebut gaya “panggilan balik kesalahan pertama”.
Konvensi tersebut adalah:
Argumen pertama dari callback
dicadangkan untuk kesalahan jika terjadi. Kemudian callback(err)
dipanggil.
Argumen kedua (dan argumen berikutnya jika diperlukan) adalah untuk hasil yang sukses. Kemudian callback(null, result1, result2…)
dipanggil.
Jadi fungsi callback
tunggal digunakan untuk melaporkan kesalahan dan meneruskan hasil.
Pada pandangan pertama, ini tampak seperti pendekatan yang layak untuk pengkodean asinkron. Dan memang benar. Untuk satu atau mungkin dua panggilan bersarang, tampaknya baik-baik saja.
Namun untuk beberapa tindakan asinkron yang mengikuti satu demi satu, kita akan memiliki kode seperti ini:
loadScript('1.js', function(kesalahan, skrip) { jika (kesalahan) { handleError(kesalahan); } kalau tidak { // ... loadScript('2.js', function(kesalahan, skrip) { jika (kesalahan) { handleError(kesalahan); } kalau tidak { // ... loadScript('3.js', function(kesalahan, skrip) { jika (kesalahan) { handleError(kesalahan); } kalau tidak { // ...lanjutkan setelah semua skrip dimuat (*) } }); } }); } });
Dalam kode di atas:
Kita load 1.js
, lalu jika tidak ada error…
Kita load 2.js
, lalu jika tidak ada error…
Kita load 3.js
, lalu jika tidak ada error – lakukan hal lain (*)
.
Ketika panggilan menjadi lebih bertumpuk, kode menjadi lebih dalam dan semakin sulit untuk dikelola, terutama jika kita memiliki kode nyata, bukan ...
yang mungkin mencakup lebih banyak loop, pernyataan kondisional, dan sebagainya.
Kadang-kadang hal ini disebut “callback hell” atau “pyramid of doom.”
“Piramida” panggilan bersarang tumbuh ke kanan dengan setiap tindakan asinkron. Segera hal itu menjadi tidak terkendali.
Jadi cara pengkodean seperti ini tidak terlalu bagus.
Kita dapat mencoba mengatasi masalah ini dengan menjadikan setiap tindakan sebagai fungsi yang berdiri sendiri, seperti ini:
loadScript('1.js', langkah1); fungsi langkah1(kesalahan, skrip) { jika (kesalahan) { handleError(kesalahan); } kalau tidak { // ... loadScript('2.js', langkah2); } } fungsi langkah2(kesalahan, skrip) { jika (kesalahan) { handleError(kesalahan); } kalau tidak { // ... loadScript('3.js', langkah3); } } fungsi langkah3(kesalahan, skrip) { jika (kesalahan) { handleError(kesalahan); } kalau tidak { // ...lanjutkan setelah semua skrip dimuat (*) } }
Melihat? Ia melakukan hal yang sama, dan sekarang tidak ada lagi sarang yang dalam karena kami menjadikan setiap tindakan sebagai fungsi tingkat atas yang terpisah.
Ini berfungsi, tetapi kodenya tampak seperti spreadsheet yang terkoyak. Sulit untuk membacanya, dan Anda mungkin memperhatikan bahwa seseorang perlu melihat-lihat bagian saat membacanya. Hal ini merepotkan, terutama jika pembaca tidak terbiasa dengan kode tersebut dan tidak tahu harus melihat ke mana.
Selain itu, fungsi bernama step*
semuanya hanya sekali pakai, dan dibuat hanya untuk menghindari “piramida malapetaka”. Tidak ada yang akan menggunakannya kembali di luar rantai tindakan. Jadi ada sedikit kekacauan namespace di sini.
Kami ingin mendapatkan sesuatu yang lebih baik.
Untungnya, ada cara lain untuk menghindari piramida semacam itu. Salah satu cara terbaik adalah dengan menggunakan “janji”, yang dijelaskan di bab berikutnya.