Mari kita kembali ke masalah yang disebutkan dalam bab Pendahuluan: callback: kita memiliki serangkaian tugas asinkron yang harus dilakukan satu demi satu — misalnya, memuat skrip. Bagaimana kita bisa mengkodekannya dengan baik?
Janji memberikan beberapa resep untuk melakukan itu.
Dalam bab ini kita membahas rangkaian janji.
Ini terlihat seperti ini:
Janji baru(fungsi(putuskan, tolak) { setTimeout(() => tekad(1), 1000); // (*) }).lalu(fungsi(hasil) { // (**) peringatan(hasil); // 1 hasil pengembalian * 2; }).lalu(fungsi(hasil) { // (***) peringatan(hasil); // 2 hasil pengembalian * 2; }).lalu(fungsi(hasil) { peringatan(hasil); // 4 hasil pengembalian * 2; });
Idenya adalah bahwa hasilnya diteruskan melalui rantai penangan .then
.
Berikut alurnya adalah:
Janji awal terselesaikan dalam 1 detik (*)
,
Kemudian handler .then
dipanggil (**)
, yang pada gilirannya menciptakan janji baru (diselesaikan dengan 2
nilai).
then
(***)
berikutnya mendapatkan hasil dari yang sebelumnya, memprosesnya (menggandakan) dan meneruskannya ke pengendali berikutnya.
…dan sebagainya.
Saat hasilnya diteruskan sepanjang rantai penangan, kita dapat melihat urutan panggilan alert
: 1
→ 2
→ 4
.
Semuanya berfungsi, karena setiap panggilan ke .then
mengembalikan janji baru, sehingga kita dapat memanggil .then
berikutnya.
Ketika seorang handler mengembalikan suatu nilai, itu menjadi hasil dari janji tersebut, sehingga .then
selanjutnya dipanggil dengan nilai tersebut.
Kesalahan klasik pemula: secara teknis kami juga dapat menambahkan banyak .then
ke satu janji. Ini bukan rantai.
Misalnya:
biarkan janji = janji baru(fungsi(putuskan, tolak) { setTimeout(() => tekad(1), 1000); }); janji.lalu(fungsi(hasil) { peringatan(hasil); // 1 hasil pengembalian * 2; }); janji.lalu(fungsi(hasil) { peringatan(hasil); // 1 hasil pengembalian * 2; }); janji.lalu(fungsi(hasil) { peringatan(hasil); // 1 hasil pengembalian * 2; });
Apa yang kami lakukan di sini hanyalah menambahkan beberapa penangan ke satu janji. Mereka tidak meneruskan hasilnya satu sama lain; sebaliknya mereka memprosesnya secara mandiri.
Berikut gambarnya (bandingkan dengan rangkaian di atas):
Semua .then
pada janji yang sama mendapatkan hasil yang sama – hasil dari janji itu. Jadi pada kode di atas semua alert
menunjukkan hal yang sama: 1
.
Dalam praktiknya kita jarang membutuhkan banyak penangan untuk satu janji. Chaining lebih sering digunakan.
Penangan, yang digunakan dalam .then(handler)
dapat membuat dan mengembalikan janji.
Dalam hal ini penangan selanjutnya menunggu hingga selesai, dan kemudian mendapatkan hasilnya.
Misalnya:
Janji baru(fungsi(putuskan, tolak) { setTimeout(() => tekad(1), 1000); }).lalu(fungsi(hasil) { peringatan(hasil); // 1 kembalikan Janji baru((putuskan, tolak) => { // (*) setTimeout(() => tekad(hasil * 2), 1000); }); }).lalu(fungsi(hasil) { // (**) peringatan(hasil); // 2 kembalikan Janji baru((putuskan, tolak) => { setTimeout(() => tekad(hasil * 2), 1000); }); }).lalu(fungsi(hasil) { peringatan(hasil); // 4 });
Di sini yang pertama .then
menampilkan 1
dan mengembalikan new Promise(…)
di baris (*)
. Setelah satu detik, masalah tersebut terselesaikan, dan hasilnya (argumen dari resolve
, ini dia result * 2
) diteruskan ke pengendali .then
yang kedua. Penangan itu ada di baris (**)
, ia menunjukkan 2
dan melakukan hal yang sama.
Jadi outputnya sama seperti contoh sebelumnya: 1 → 2 → 4, tapi sekarang dengan jeda 1 detik di antara panggilan alert
.
Pengembalian janji memungkinkan kita membangun rantai tindakan asinkron.
Mari gunakan fitur ini dengan loadScript
yang dijanjikan, yang ditentukan di bab sebelumnya, untuk memuat skrip satu per satu, secara berurutan:
loadScript("https://javascript.info/article/promise-chaining/one.js") .then(fungsi(skrip) { return loadScript("https://javascript.info/article/promise-chaining/two.js"); }) .then(fungsi(skrip) { return loadScript("https://javascript.info/article/promise-chaining/three.js"); }) .then(fungsi(skrip) { // menggunakan fungsi yang dideklarasikan dalam skrip // untuk menunjukkan bahwa mereka memang dimuat satu(); dua(); tiga(); });
Kode ini dapat dibuat lebih pendek dengan fungsi panah:
loadScript("https://javascript.info/article/promise-chaining/one.js") .then(script => loadScript("https://javascript.info/article/promise-chaining/two.js")) .then(script => loadScript("https://javascript.info/article/promise-chaining/three.js")) .lalu(skrip => { // skrip dimuat, kita dapat menggunakan fungsi yang dideklarasikan di sana satu(); dua(); tiga(); });
Di sini, setiap panggilan loadScript
mengembalikan sebuah janji, dan .then
berikutnya dijalankan ketika masalah tersebut terselesaikan. Kemudian memulai pemuatan skrip berikutnya. Jadi skrip dimuat satu demi satu.
Kita dapat menambahkan lebih banyak tindakan asinkron ke dalam rantai. Harap dicatat bahwa kodenya masih “datar” — ia tumbuh ke bawah, bukan ke kanan. Tidak ada tanda-tanda “piramida malapetaka”.
Secara teknis, kita dapat menambahkan .then
langsung ke setiap loadScript
, seperti ini:
loadScript("https://javascript.info/article/promise-chaining/one.js").lalu(skrip1 => { loadScript("https://javascript.info/article/promise-chaining/two.js").then(script2 => { loadScript("https://javascript.info/article/promise-chaining/three.js").lalu(skrip3 => { // fungsi ini memiliki akses ke variabel script1, script2 dan script3 satu(); dua(); tiga(); }); }); });
Kode ini melakukan hal yang sama: memuat 3 skrip secara berurutan. Tapi itu “tumbuh ke kanan”. Jadi kami memiliki masalah yang sama dengan panggilan balik.
Orang yang mulai menggunakan janji terkadang tidak tahu tentang rantai, jadi mereka menulisnya seperti ini. Umumnya, rangkaian lebih disukai.
Terkadang tidak masalah untuk menulis .then
secara langsung, karena fungsi yang disarangkan memiliki akses ke cakupan luar. Dalam contoh di atas, callback paling bersarang memiliki akses ke semua variabel script1
, script2
, script3
. Tapi itu lebih merupakan pengecualian dan bukan aturan.
barang kemudian
Tepatnya, seorang handler mungkin tidak mengembalikan sebuah janji, namun sebuah objek yang disebut “thenable” – sebuah objek arbitrer yang memiliki metode .then
. Itu akan diperlakukan sama seperti janji.
Idenya adalah bahwa perpustakaan pihak ketiga dapat mengimplementasikan objek mereka sendiri yang “sesuai dengan janji”. Mereka dapat memiliki serangkaian metode yang diperluas, namun juga kompatibel dengan janji asli, karena mereka menerapkan .then
.
Berikut ini contoh objek yang dapat digunakan:
kelas Kemudian dapat { konstruktor(angka) { this.num = angka; } lalu(putuskan, tolak) { waspada(menyelesaikan); // fungsi() { kode asli } // selesaikan dengan this.num*2 setelah 1 detik setTimeout(() => tekad(ini.angka * 2), 1000); // (**) } } Janji baru(tekad => tekad(1)) .lalu(hasil => { return new Thenable(hasil); // (*) }) .then(waspada); // menampilkan 2 setelah 1000 ms
JavaScript memeriksa objek yang dikembalikan oleh handler .then
di baris (*)
: jika objek tersebut memiliki metode yang dapat dipanggil bernama then
, maka ia akan memanggil metode tersebut dengan menyediakan fungsi asli resolve
, reject
sebagai argumen (mirip dengan eksekutor) dan menunggu hingga salah satu dari objek tersebut disebut. Dalam contoh di atas resolve(2)
dipanggil setelah 1 detik (**)
. Kemudian hasilnya diteruskan lebih jauh ke bawah dalam rantai.
Fitur ini memungkinkan kita untuk mengintegrasikan objek khusus dengan rantai janji tanpa harus mewarisi dari Promise
.
Dalam pemrograman frontend, janji sering digunakan untuk permintaan jaringan. Jadi, mari kita lihat contoh selengkapnya.
Kami akan menggunakan metode pengambilan untuk memuat informasi tentang pengguna dari server jauh. Ini memiliki banyak parameter opsional yang dibahas dalam bab terpisah, tetapi sintaks dasarnya cukup sederhana:
biarkan janji = ambil(url);
Ini membuat permintaan jaringan ke url
dan mengembalikan janji. Janji diselesaikan dengan objek response
ketika server jarak jauh merespons dengan header, tetapi sebelum respons penuh diunduh .
Untuk membaca respon lengkap, kita harus memanggil metode response.text()
: metode ini mengembalikan janji yang terselesaikan ketika teks lengkap diunduh dari server jarak jauh, dengan teks tersebut sebagai hasilnya.
Kode di bawah ini membuat permintaan ke user.json
dan memuat teksnya dari server:
ambil('https://javascript.info/article/promise-chaining/user.json') // .lalu di bawah ini berjalan ketika server jarak jauh merespons .then(fungsi(respon) { // respon.teks() mengembalikan janji baru yang diselesaikan dengan teks respons lengkap // saat dimuat kembalikan respon.teks(); }) .then(fungsi(teks) { // ...dan inilah isi dari file jarak jauh peringatan(teks); // {"nama": "iliakan", "isAdmin": true} });
Objek response
yang dikembalikan dari fetch
juga menyertakan metode response.json()
yang membaca data jarak jauh dan menguraikannya sebagai JSON. Dalam kasus kami, ini lebih nyaman, jadi mari kita beralih ke sana.
Kami juga akan menggunakan fungsi panah agar singkatnya:
// sama seperti di atas, tetapi respon.json() mem-parsing konten jarak jauh sebagai JSON ambil('https://javascript.info/article/promise-chaining/user.json') .then(respon => respon.json()) .then(pengguna => peringatan(nama pengguna)); // iliakan, mendapat nama pengguna
Sekarang mari kita lakukan sesuatu dengan pengguna yang dimuat.
Misalnya, kita dapat membuat satu permintaan lagi ke GitHub, memuat profil pengguna dan menampilkan avatar:
// Buat permintaan untuk pengguna.json ambil('https://javascript.info/article/promise-chaining/user.json') // Muat sebagai json .then(respon => respon.json()) // Buat permintaan ke GitHub .then(pengguna => ambil(`https://api.github.com/users/${user.name}`)) // Muat respons sebagai json .then(respon => respon.json()) // Tampilkan gambar avatar (githubUser.avatar_url) selama 3 detik (mungkin dianimasikan) .lalu(githubUser => { biarkan img = dokumen.createElement('img'); img.src = githubUser.avatar_url; img.className = "contoh-avatar-janji"; dokumen.body.append(img); setTimeout(() => img.hapus(), 3000); // (*) });
Kode ini berfungsi; lihat komentar tentang detailnya. Namun, ada potensi masalah di dalamnya, kesalahan umum bagi mereka yang mulai menggunakan janji.
Lihat pada baris (*)
: bagaimana kita dapat melakukan sesuatu setelah avatar selesai ditampilkan dan dihapus? Misalnya, kami ingin memperlihatkan formulir untuk mengedit pengguna itu atau sesuatu yang lain. Untuk saat ini, tidak mungkin.
Agar rantainya bisa diperpanjang, kita perlu mengembalikan janji yang terselesaikan saat avatar selesai ditampilkan.
Seperti ini:
ambil('https://javascript.info/article/promise-chaining/user.json') .then(respon => respon.json()) .then(pengguna => ambil(`https://api.github.com/users/${user.name}`)) .then(respon => respon.json()) .then(githubUser => janji baru(fungsi(putuskan, tolak) { // (*) biarkan img = dokumen.createElement('img'); img.src = githubUser.avatar_url; img.className = "contoh-avatar-janji"; dokumen.body.append(img); setWaktu habis(() => { img.hapus(); tekad(githubUser); // (**) }, 3000); })) // terpicu setelah 3 detik .then(githubUser => alert(`Selesai menampilkan ${githubUser.name}`));
Artinya, pengendali .then
di baris (*)
sekarang mengembalikan new Promise
, yang diselesaikan hanya setelah pemanggilan resolve(githubUser)
di setTimeout
(**)
. Berikutnya .then
dalam rantai akan menunggu untuk itu.
Sebagai praktik yang baik, tindakan asinkron harus selalu menghasilkan janji. Hal ini memungkinkan untuk merencanakan tindakan setelahnya; bahkan jika kita tidak berencana untuk memperluas rantai ini sekarang, kita mungkin memerlukannya nanti.
Terakhir, kita dapat membagi kode menjadi fungsi yang dapat digunakan kembali:
fungsi loadJson(url) { kembali ambil(url) .then(respon => respon.json()); } fungsi loadGithubUser(nama) { return loadJson(`https://api.github.com/users/${name}`); } fungsi showAvatar(githubUser) { kembalikan Janji baru(fungsi(putuskan, tolak) { biarkan img = dokumen.createElement('img'); img.src = githubUser.avatar_url; img.className = "contoh-avatar-janji"; dokumen.body.append(img); setWaktu habis(() => { img.hapus(); tekad(githubUser); }, 3000); }); } // Gunakan: loadJson('https://javascript.info/article/promise-chaining/user.json') .then(pengguna => loadGithubUser(nama pengguna)) .lalu(tampilkanAvatar) .then(githubUser => alert(`Selesai menampilkan ${githubUser.name}`)); // ...
Jika penangan .then
(atau catch/finally
, tidak masalah) mengembalikan sebuah janji, sisa rantai menunggu hingga janji tersebut diselesaikan. Jika hal ini terjadi, hasilnya (atau kesalahannya) diteruskan lebih lanjut.
Berikut gambaran lengkapnya:
Apakah potongan kode ini sama? Dengan kata lain, apakah mereka berperilaku sama dalam keadaan apa pun, untuk fungsi pengendali apa pun?
janji.lalu(f1).catch(f2);
Melawan:
janji.lalu(f1, f2);
Jawaban singkatnya adalah: tidak, keduanya tidak setara :
Bedanya, jika terjadi kesalahan pada f1
, maka ditangani oleh .catch
di sini:
janji .lalu(f1) .menangkap(f2);
…Tapi tidak di sini:
janji .lalu(f1, f2);
Itu karena kesalahan diturunkan ke rantai, dan pada potongan kode kedua tidak ada rantai di bawah f1
.
Dengan kata lain, .then
meneruskan results/errors ke .then/catch
berikutnya. Jadi pada contoh pertama, ada catch
di bawah, dan pada contoh kedua tidak ada, jadi kesalahannya tidak tertangani.