Bayangkan Anda seorang penyanyi top, dan penggemar bertanya siang dan malam tentang lagu Anda yang akan datang.
Untuk mendapatkan keringanan, Anda berjanji untuk mengirimkannya kepada mereka ketika sudah diterbitkan. Anda memberikan daftar kepada penggemar Anda. Mereka bisa mengisi alamat emailnya, sehingga ketika lagu sudah tersedia, semua pihak yang berlangganan langsung menerimanya. Dan bahkan jika ada yang tidak beres, katakanlah, kebakaran di studio sehingga Anda tidak dapat memublikasikan lagunya, mereka akan tetap diberi tahu.
Semua orang senang: kamu, karena orang-orang tidak lagi memadatimu, dan penggemar, karena mereka tidak akan ketinggalan lagunya.
Ini adalah analogi kehidupan nyata untuk hal-hal yang sering kita temui dalam pemrograman:
Sebuah “kode produksi” yang melakukan sesuatu dan membutuhkan waktu. Misalnya, beberapa kode yang memuat data melalui jaringan. Itu adalah "penyanyi".
Sebuah “kode konsumsi” yang menginginkan hasil dari “kode produksi” setelah siap. Banyak fungsi yang mungkin memerlukan hasil itu. Ini adalah “penggemar”.
Promise adalah objek JavaScript khusus yang menghubungkan “kode produksi” dan “kode konsumsi” secara bersamaan. Dalam analogi kami: ini adalah “daftar langganan”. "Kode produksi" membutuhkan waktu berapa pun untuk menghasilkan hasil yang dijanjikan, dan "janji" membuat hasil tersebut tersedia untuk semua kode yang berlangganan ketika sudah siap.
Analoginya tidak terlalu akurat, karena janji JavaScript lebih kompleks daripada daftar langganan sederhana: janji tersebut memiliki fitur dan batasan tambahan. Tapi tidak apa-apa untuk memulainya.
Sintaks konstruktor untuk objek janji adalah:
biarkan janji = janji baru(fungsi(putuskan, tolak) { // eksekutor (kode penghasil, "penyanyi") });
Fungsi yang diteruskan ke new Promise
disebut executor . Ketika new Promise
dibuat, eksekutor berjalan secara otomatis. Ini berisi kode produksi yang pada akhirnya akan menghasilkan hasilnya. Jika dianalogikan di atas: eksekutor adalah “penyanyi”.
Argumen resolve
dan reject
adalah panggilan balik yang disediakan oleh JavaScript itu sendiri. Kode kita hanya ada di dalam eksekutor.
Saat eksekutor mendapatkan hasilnya, cepat atau lambat, tidak masalah, eksekutor harus memanggil salah satu callback berikut:
resolve(value)
— jika pekerjaan berhasil diselesaikan, dengan value
hasil.
reject(error)
— jika terjadi kesalahan, error
adalah objek kesalahannya.
Jadi untuk meringkas: pelaksana berjalan secara otomatis dan mencoba melakukan suatu pekerjaan. Ketika upaya selesai, ia akan memanggil resolve
jika berhasil atau reject
jika ada kesalahan.
Objek promise
yang dikembalikan oleh konstruktor new Promise
memiliki properti internal berikut:
state
— awalnya "pending"
, lalu berubah menjadi "fulfilled"
saat resolve
dipanggil atau "rejected"
saat reject
dipanggil.
result
— awalnya undefined
, lalu berubah menjadi value
saat resolve(value)
dipanggil atau error
saat reject(error)
dipanggil.
Jadi pelaksana akhirnya memindahkan promise
ke salah satu negara bagian berikut:
Nanti kita akan melihat bagaimana “penggemar” dapat berlangganan perubahan ini.
Berikut ini contoh konstruktor janji dan fungsi eksekutor sederhana dengan “memproduksi kode” yang membutuhkan waktu (melalui setTimeout
):
biarkan janji = janji baru(fungsi(putuskan, tolak) { // fungsi dijalankan secara otomatis saat janji dibuat // setelah 1 detik menandakan pekerjaan selesai dengan hasil "selesai" setTimeout(() => tekad("selesai"), 1000); });
Kita dapat melihat dua hal dengan menjalankan kode di atas:
Pelaksana dipanggil secara otomatis dan segera (dengan new Promise
).
Pelaksana menerima dua argumen: resolve
dan reject
. Fungsi-fungsi ini sudah ditentukan sebelumnya oleh mesin JavaScript, jadi kita tidak perlu membuatnya. Kita sebaiknya hanya memanggil salah satu dari mereka jika sudah siap.
Setelah satu detik "pemrosesan", pelaksana memanggil resolve("done")
untuk menghasilkan hasilnya. Ini mengubah keadaan objek promise
:
Itu adalah contoh penyelesaian pekerjaan yang berhasil, “janji yang dipenuhi”.
Dan sekarang contoh eksekutor yang menolak janji dengan kesalahan:
biarkan janji = janji baru(fungsi(putuskan, tolak) { // setelah 1 detik memberi sinyal bahwa pekerjaan selesai dengan kesalahan setTimeout(() => reject(Kesalahan baru("Ups!")), 1000); });
Panggilan ke reject(...)
memindahkan objek janji ke status "rejected"
:
Ringkasnya, pelaksana harus melakukan suatu pekerjaan (biasanya sesuatu yang membutuhkan waktu) dan kemudian memanggil resolve
atau reject
untuk mengubah keadaan objek janji yang bersangkutan.
Sebuah janji yang diselesaikan atau ditolak disebut “menyelesaikan”, bukan janji yang awalnya “menunggu keputusan”.
Hanya ada satu hasil atau kesalahan
Pelaksana harus memanggil hanya satu resolve
atau satu reject
. Setiap perubahan negara bagian bersifat final.
Semua seruan resolve
dan reject
lebih lanjut diabaikan:
biarkan janji = janji baru(fungsi(putuskan, tolak) { tekad("selesai"); tolak(Kesalahan baru("...")); // diabaikan setTimeout(() => tekad("...")); // diabaikan });
Idenya adalah bahwa pekerjaan yang dilakukan oleh pelaksana mungkin hanya mempunyai satu hasil atau kesalahan.
Selain itu, resolve
/ reject
hanya mengharapkan satu argumen (atau tidak sama sekali) dan akan mengabaikan argumen tambahan.
Tolak dengan objek Error
Jika terjadi kesalahan, pelaksana harus memanggil reject
. Hal ini dapat dilakukan dengan argumen jenis apa pun (seperti resolve
). Namun disarankan untuk menggunakan objek Error
(atau objek yang mewarisi dari Error
). Alasannya akan segera menjadi jelas.
Segera menelepon resolve
/ reject
Dalam praktiknya, pelaksana biasanya melakukan sesuatu secara asinkron dan memanggil resolve
/ reject
setelah beberapa waktu, tetapi hal ini tidak harus dilakukan. Kita juga bisa langsung memanggil resolve
atau reject
, seperti ini:
biarkan janji = janji baru(fungsi(putuskan, tolak) { // tidak meluangkan waktu untuk melakukan pekerjaan itu tekad(123); // langsung berikan hasilnya: 123 });
Misalnya, hal ini mungkin terjadi ketika kita mulai melakukan suatu pekerjaan tetapi kemudian melihat bahwa semuanya telah selesai dan disimpan dalam cache.
Tidak apa-apa. Kami segera memiliki janji yang terselesaikan.
state
dan result
bersifat internal
state
properti dan result
objek Promise bersifat internal. Kami tidak dapat mengaksesnya secara langsung. Kita bisa menggunakan metode .then
/ .catch
/ .finally
untuk itu. Hal tersebut dijelaskan di bawah ini.
Objek Promise berfungsi sebagai penghubung antara eksekutor (“kode penghasil” atau “penyanyi”) dan fungsi konsumsi (“penggemar”), yang akan menerima hasil atau kesalahan. Fungsi konsumsi dapat didaftarkan (berlangganan) menggunakan metode .then
dan .catch
.
Yang paling penting dan mendasar adalah .then
.
Sintaksnya adalah:
janji.lalu( function(hasil) { /* menangani hasil yang berhasil */ }, fungsi(kesalahan) { /* menangani kesalahan */ } );
Argumen pertama dari .then
adalah fungsi yang berjalan ketika janji diselesaikan dan menerima hasilnya.
Argumen kedua dari .then
adalah fungsi yang berjalan ketika janji ditolak dan menerima kesalahan.
Misalnya, inilah reaksi terhadap janji yang berhasil diselesaikan:
biarkan janji = janji baru(fungsi(putuskan, tolak) { setTimeout(() => tekad("selesai!"), 1000); }); // tekad menjalankan fungsi pertama di .then janji.lalu( hasil => peringatan(hasil), // menampilkan "selesai!" setelah 1 detik error => alert(error) // tidak berjalan );
Fungsi pertama dijalankan.
Dan jika terjadi penolakan, yang kedua:
biarkan janji = janji baru(fungsi(putuskan, tolak) { setTimeout(() => reject(Kesalahan baru("Ups!")), 1000); }); // reject menjalankan fungsi kedua di .then janji.lalu( hasil => peringatan(hasil), // tidak berjalan error => alert(error) // menampilkan "Error: Ups!" setelah 1 detik );
Jika kami hanya tertarik pada penyelesaian yang berhasil, maka kami hanya dapat memberikan satu argumen fungsi ke .then
:
biarkan janji = Janji baru(putuskan => { setTimeout(() => tekad("selesai!"), 1000); }); janji.lalu(peringatan); // menunjukkan "selesai!" setelah 1 detik
Jika kita hanya tertarik pada kesalahan, maka kita dapat menggunakan null
sebagai argumen pertama: .then(null, errorHandlingFunction)
. Atau kita bisa menggunakan .catch(errorHandlingFunction)
, yang persis sama:
biarkan janji = janji baru((putuskan, tolak) => { setTimeout(() => reject(Kesalahan baru("Ups!")), 1000); }); // .catch(f) sama dengan janji.then(null, f) janji.catch(alert); // menampilkan "Kesalahan: Ups!" setelah 1 detik
Panggilan .catch(f)
adalah analog lengkap dari .then(null, f)
, itu hanya singkatan.
Sama seperti ada klausa finally
dalam try {...} catch {...}
biasa, finally
ada janji.
Panggilan .finally(f)
mirip dengan .then(f, f)
dalam arti bahwa f
selalu berjalan, ketika janji diselesaikan: baik itu diselesaikan atau ditolak.
Ide dari finally
adalah menyiapkan handler untuk melakukan pembersihan/penyelesaian setelah operasi sebelumnya selesai.
Misalnya menghentikan indikator pemuatan, menutup koneksi yang tidak diperlukan lagi, dll.
Anggap saja sebagai penutup pesta. Tidak peduli apakah pestanya baik atau buruk, berapa banyak teman yang hadir, kita tetap perlu (atau setidaknya harus) melakukan pembersihan setelahnya.
Kodenya mungkin terlihat seperti ini:
Janji baru((putuskan, tolak) => { /* melakukan sesuatu yang memerlukan waktu, lalu memanggil penyelesaian atau mungkin penolakan */ }) // berjalan ketika janji sudah dilunasi, tidak masalah berhasil atau tidak .finally(() => berhenti memuat indikator) // jadi indikator pemuatan selalu dihentikan sebelum kita melanjutkan .then(hasil => tampilkan hasil, err => tampilkan kesalahan)
Harap dicatat bahwa finally(f)
bukanlah alias dari then(f,f)
.
Ada perbedaan penting:
Penangan finally
tidak memiliki argumen. Pada finally
kita tidak tahu apakah janji itu berhasil atau tidak. Tidak apa-apa, karena tugas kita biasanya adalah melakukan prosedur penyelesaian “umum”.
Silakan lihat contoh di atas: seperti yang Anda lihat, pengendali finally
tidak memiliki argumen, dan hasil janji ditangani oleh pengendali berikutnya.
Penangan finally
“melewati” hasil atau kesalahan ke penangan berikutnya yang sesuai.
Misalnya, di sini hasilnya finally
ke then
:
Janji baru((putuskan, tolak) => { setTimeout(() => tekad("nilai"), 2000); }) .finally(() => alert("Janji siap")) // terpicu terlebih dahulu .then(hasil => peringatan(hasil)); // <-- .lalu tampilkan "nilai"
Seperti yang Anda lihat, value
yang dikembalikan oleh janji pertama diteruskan finally
ke then
berikutnya.
Itu sangat mudah, karena finally
tidak dimaksudkan untuk memproses hasil yang dijanjikan. Seperti yang dikatakan, ini adalah tempat untuk melakukan pembersihan umum, apa pun hasilnya.
Dan inilah contoh kesalahannya, agar kita dapat melihat bagaimana kesalahan tersebut finally
dapat catch
:
Janji baru((putuskan, tolak) => { melempar Kesalahan baru("kesalahan"); }) .finally(() => alert("Janji siap")) // terpicu terlebih dahulu .catch(err => waspada(err)); // <-- .catch menunjukkan kesalahan
Penangan finally
juga tidak boleh mengembalikan apa pun. Jika ya, nilai yang dikembalikan akan diabaikan secara diam-diam.
Satu-satunya pengecualian terhadap aturan ini adalah ketika pengendali finally
melakukan kesalahan. Kemudian kesalahan ini berpindah ke penangan berikutnya, bukan ke hasil sebelumnya.
Untuk meringkas:
Penangan finally
tidak mendapatkan hasil dari penangan sebelumnya (tidak ada argumen). Hasil ini diteruskan ke penangan berikutnya yang sesuai.
Jika pengendali finally
mengembalikan sesuatu, itu akan diabaikan.
Ketika finally
terjadi kesalahan, maka eksekusi diteruskan ke penangan kesalahan terdekat.
Fitur-fitur ini sangat membantu dan membuat segala sesuatunya berjalan sebagaimana mestinya jika finally
kita menggunakan cara yang seharusnya digunakan: untuk prosedur pembersihan umum.
Kita dapat melampirkan penangan pada janji yang telah diselesaikan
Jika sebuah janji tertunda, penangan .then/catch/finally
menunggu hasilnya.
Terkadang, mungkin sebuah janji telah diselesaikan ketika kita menambahkan pengendali ke dalamnya.
Jika demikian, penangan ini langsung dijalankan:
// janji tersebut diselesaikan segera setelah dibuat biarkan janji = janji baru(tekad => tekad("selesai!")); janji.lalu(peringatan); // Selesai! (muncul sekarang)
Perhatikan bahwa hal ini membuat janji lebih kuat daripada skenario “daftar langganan” di kehidupan nyata. Jika penyanyi telah merilis lagunya dan kemudian seseorang mendaftar ke daftar berlangganan, kemungkinan besar dia tidak akan menerima lagu tersebut. Berlangganan di kehidupan nyata harus dilakukan sebelum acara.
Janji lebih fleksibel. Kita dapat menambahkan penangan kapan saja: jika hasilnya sudah ada, mereka tinggal mengeksekusinya.
Selanjutnya, mari kita lihat contoh yang lebih praktis tentang bagaimana janji dapat membantu kita menulis kode asinkron.
Kita punya fungsi loadScript
untuk memuat skrip dari bab sebelumnya.
Inilah varian berbasis panggilan balik, sekadar untuk mengingatkan kita akan hal itu:
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); }
Mari kita menulis ulang menggunakan Promises.
Fungsi baru loadScript
tidak memerlukan panggilan balik. Sebaliknya, ini akan membuat dan mengembalikan objek Promise yang diselesaikan ketika pemuatan selesai. Kode luar dapat menambahkan penangan (fungsi berlangganan) ke dalamnya menggunakan .then
:
fungsi loadScript(src) { kembalikan Janji baru(fungsi(putuskan, tolak) { biarkan skrip = document.createElement('script'); skrip.src = src; script.onload = () => tekad(skrip); script.onerror = () => reject(new Error(`Kesalahan pemuatan skrip untuk ${src}`)); document.head.append(skrip); }); }
Penggunaan:
biarkan janji = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js"); janji.lalu( skrip => peringatan(`${script.src} dimuat!`), kesalahan => peringatan(`Kesalahan: ${kesalahan.pesan}`) ); janji.lalu(skrip => alert('Penangan lain...'));
Kita dapat langsung melihat beberapa manfaat dibandingkan pola berbasis panggilan balik:
Janji | Panggilan balik |
---|---|
Janji memungkinkan kita melakukan segala sesuatunya sesuai dengan tatanan alamiah. Pertama, kita jalankan loadScript(script) , dan .then kita tulis apa yang harus dilakukan dengan hasilnya. | Kita harus memiliki fungsi callback saat memanggil loadScript(script, callback) . Dengan kata lain, kita harus mengetahui apa yang harus dilakukan dengan hasilnya sebelum loadScript dipanggil. |
Kita dapat menghubungi .then pada Promise sebanyak yang kita inginkan. Setiap kali, kami menambahkan “penggemar” baru, fungsi berlangganan baru, ke “daftar langganan”. Lebih lanjut tentang ini di bab berikutnya: Rantai janji. | Hanya ada satu panggilan balik. |
Jadi janji memberi kita aliran kode dan fleksibilitas yang lebih baik. Tapi masih ada lagi. Kita akan melihatnya di bab berikutnya.
Apa output dari kode di bawah ini?
biarkan janji = janji baru(fungsi(putuskan, tolak) { tekad(1); setTimeout(() => tekad(2), 1000); }); janji.lalu(peringatan);
Outputnya adalah: 1
.
Panggilan kedua untuk resolve
diabaikan, karena hanya panggilan pertama untuk reject/resolve
yang diperhitungkan. Panggilan selanjutnya diabaikan.
Fungsi bawaan setTimeout
menggunakan panggilan balik. Ciptakan alternatif berbasis janji.
Fungsi delay(ms)
harus mengembalikan janji. Janji itu akan terselesaikan setelah ms
milidetik, sehingga kita dapat menambahkan .then
ke dalamnya, seperti ini:
penundaan fungsi (ms) { // kode Anda } delay(3000).then(() => alert('berjalan setelah 3 detik'));
penundaan fungsi (ms) { kembalikan Janji baru(resolve => setTimeout(resolve, ms)); } delay(3000).then(() => alert('berjalan setelah 3 detik'));
Harap dicatat bahwa dalam resolve
tugas ini dipanggil tanpa argumen. Kami tidak mengembalikan nilai apa pun dari delay
, hanya memastikan penundaannya.
Tulis ulang fungsi showCircle
dalam penyelesaian tugas Lingkaran animasi dengan panggilan balik sehingga mengembalikan janji alih-alih menerima panggilan balik.
Penggunaan baru:
tampilkanLingkaran(150, 150, 100).lalu(div => { div.classList.add('bola pesan'); div.append("Halo dunia!"); });
Ambil solusi dari tugas Lingkaran animasi dengan panggilan balik sebagai dasarnya.
Buka solusi di kotak pasir.