15 Desember 2021

Promises chaining

Mari kembali ke masalah yang disebutkan di dalam bab Pengenalan: callback: kita memiliki sebuah urutan tugas asynchronous untuk dilakukan satu demi satu. Sebagai contoh, memuat scripts. Bagaimana kita bisa membuat kodenya dengan baik?

Promises menyediakan beberapa resep untuk melakukannya.

Di bab ini kita melibatkan promise chaining.

Itu terlihat seperti ini:

new Promise(function (resolve, reject) {
  setTimeout(() => resolve(1), 1000); // (*)
})
  .then(function (result) {
    // (**)

    alert(result); // 1
    return result * 2;
  })
  .then(function (result) {
    // (***)

    alert(result); // 2
    return result * 2;
  })
  .then(function (result) {
    alert(result); // 4
    return result * 2;
  });

Idenya adalah bahwa result diteruskan melalui rantai handlers .then.

Ini alurnya:

  1. Promise pertama selesai dalam 1 detik (*),
  2. Kemudian handler .then dipanggil (**).
  3. Nilai yang dikembalikan diteruskan ke handler .then selanjutnya (***)
  4. ???dan seterusnya.

Selama result diteruskan di sepanjang rantai handlers, kita bisa melihat urutan pemanggilan alert: 1 ??? 2 ??? 4.

Seluruhnya bekerja, karena pemanggilan ke promise.then mengembalikan sebuah promise, jadi kita bisa memanggil .then selanjutnya.

Ketika sebuah handler mengembalikan nilai, handler tersebut menjadi hasil dari promise, jadi .then selanjutnya dipanggil dengan itu.

Kesalahan klasik pemula: secara teknis kita juga dapat menambahkan banyak .then ke satu promise. Ini bukan chaining.

Sebagai contoh:

let promise = new Promise(function (resolve, reject) {
  setTimeout(() => resolve(1), 1000);
});

promise.then(function (result) {
  alert(result); // 1
  return result * 2;
});

promise.then(function (result) {
  alert(result); // 1
  return result * 2;
});

promise.then(function (result) {
  alert(result); // 1
  return result * 2;
});

Apa yang kita lakukan di sini hanya beberapa handlers untuk satu promise. Handlers tersebut tidak meneruskan result ke satu sama lain, melainkan memprosesnya masing-masing.

Ini gambarnya (bandingkan dengan chaining di atas):

Semua .then pada promise yang sama mendapatkan result yang sama ??? result dari promise. Jadi di dalam kode di atas semua alert menunjukkan yang sama: 1.

Dalam prakteknya kita jarang membutuhkan banyak handlers untuk satu promise. Chaining lebih sering digunakan.

Mengembalikan promises

Sebuah handler, yang digunakan di dalam .then(handler) dapat membuat dan mengambalikan sebuah promise.

Dalam hal ini handlers selanjutnya menunggu sampai mengendap, dan kemudian mendapatkan hasilnya.

Sebagai contoh:

new Promise(function(resolve, reject) {

  setTimeout(() => resolve(1), 1000);

}).then(function(result) {

  alert(result); // 1

  return new Promise((resolve, reject) => { // (*)
    setTimeout(() => resolve(result * 2), 1000);
  });

}).then(function(result) { // (**)

  alert(result); // 2

  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(result * 2), 1000);
  });

}).then(function(result) {

  alert(result); // 4

});

Di sini .then pertama menunjukan 1 dan mengembalikan new Promise(???) pada baris (*). Setelah satu detik selesai, dan hasil (argument resolve, di sini result * 2) diteruskan ke handler .then kedua. Handler pada baris (**), menunjukan 2 dan melakukan hal yang sama.

Jadi keluarannya sama dengan contoh sebelumnya: 1 ??? 2 ??? 4, tetapi sekarang dengan menunda 1 detik antara pemanggilan alert.

Mengembalikan promises memperbolehkan kita untuk membangun rantai aksi asynchronous.

Contoh: loadScript

Mari menggunakan fitur ini dengan promisified loadScript, didefinisikan di bab sebelumnya, untuk memuat scripts satu demi satu, secara berurutan:

loadScript("/article/promise-chaining/one.js")
  .then(function (script) {
    return loadScript("/article/promise-chaining/two.js");
  })
  .then(function (script) {
    return loadScript("/article/promise-chaining/three.js");
  })
  .then(function (script) {
    // gunakan functions yang dideklarasikan di dalam scripts
    // untuk menunjukkan bahwa functions memang dimuat
    one();
    two();
    three();
  });

Kode ini bisa lebih ringkas dengan arrow functions:

loadScript("/article/promise-chaining/one.js")
  .then((script) => loadScript("/article/promise-chaining/two.js"))
  .then((script) => loadScript("/article/promise-chaining/three.js"))
  .then((script) => {
    // scripts dimuat, kita dapat menggunakan functions yang dideklarasikan di sini
    one();
    two();
    three();
  });

Di sini setiap pemanggilan loadScript mengembalikkan sebuah promise, dan .then selanjutnya berjalan ketika promise selesai. Kemudian memulai pemuatan script selanjutnya. Jadi scripts dimuat satu setelah yang lain.

Kita dapat menambahkan lagi aksi asynchronous ke rantainya. Harap catat bahwa kodenya masih ???flat???, kodenya tumbuh ke bawah bukan ke kanan. Tidak ada tanda-tanda ???pyramid of doom???.

Secara teknis, kita dapat menambahkan .then secara langsung ke setiap loadScript, seperti ini:

loadScript("/article/promise-chaining/one.js").then((script1) => {
  loadScript("/article/promise-chaining/two.js").then((script2) => {
    loadScript("/article/promise-chaining/three.js").then((script3) => {
      // this function has access to variables script1, script2 and script3
      one();
      two();
      three();
    });
  });
});

Kode ini melakukan hal yang sama: muat 3 scripts berurutan. Tetapi ???tumbuh ke kanan???. Jadi kita punya masalah yang sama dengan callbacks.

Orang yang baru memulai untuk menggunakan promises kadang-kadang tidak tahu tentang chaining, jadi mereka menulisnya dengan cara ini. Umumnya, chaining lebih disukai.

Terkadang ok untuk menulis .then secara langsung, karena function bersarang memiliki akses ke luar scope. Pada contoh di atas callback paling bertingkat memiliki akses ke semua variabel script1, script2, script3. Tetapi itu pengecualian bukan aturan.

Thenables

Tepatnya, sebuah handler mungkin tidak mengembalikkan sebuah promise, tetapi dipanggil objek ???thenable??? ??? sebuah objek sewenang-wenang yang memiliki method .then, dan diperlakukan sama seperti sebuah promise.

Idenya adalah bahwa pustaka 3rd-party dapat menerapkan objek ???promise-compatible??? mereka sendiri. Mereka dapat memiliki serangkaian methods yang luas, tetapi juga kompatibel dengan promises asli, karena mereka menerapkan .then.

Ini contoh dari objek thenable:

class Thenable {
  constructor(num) {
    this.num = num;
  }
  then(resolve, reject) {
    alert(resolve); // function() { native code }
    // selesai dengan this.num*2 setelah 1 detik
    setTimeout(() => resolve(this.num * 2), 1000); // (**)
  }
}

new Promise(resolve => resolve(1))
  .then(result => {
    return new Thenable(result); // (*)
  })
  .then(alert); // menunjukkan 2 setelah 1000ms

JavaScript memeriksa objek yang dikembalikkan oleh handler .then di baris (*): jika ada method callable yang bernama then, kemudian method tersebut memanggil method yang menyediakan functions resolve, reject asli sebagai arguments (mirip ke eksekutor) dan menunggu sampai satu dari mereka dipanggil. Pada contoh di atas resolve(2) dipanggil setelah 1 detik (**). Kemudian result diteruskan ke bawah chain.

Fitur ini memperbolehkan kita untuk untuk mengintegrasikan objek kustom dengan promise chains tanpa memiliki pewarisan dari Promise.

Contoh Terbesar: fetch

Di dalam pemrograman frontend promises sering digunakan untuk permintaan jaringan. Jadi mari lihat contoh yang lebih luas dari itu.

Kita akan menggunakan methos fetch untuk memuat informasi tentang pengguna dari server jarak jauh. Banyak sekali pilihan parameter yang dilibatkan di dalam bab terpisah, tetapi sintaksis dasar cukup sederhana:

let promise = fetch(url);

Ini membuat permintaan jaringan ke url dan mengembalikkan sebuah promise. Promise selesai dengan objek response ketika server jarak jauh merespon dengan header, tetapi sebelum response penuh diunduh.

Untuk membaca response penuh, kita harus memanggil sebuah method response.text(): method tersebut mengembalikkan sebuah promise yang selesai ketika teks penuh ull telah diunduh dari server jarak jauh, dengan teks tersebut sebagai hasilnya.

Kode di bawah ini membuat permintaan ke user.json dan memuat teks dari server:

fetch("/article/promise-chaining/user.json")
  // .then di bawah berjalan ketika server jarak jauh merespon
  .then(function (response) {
    // response.text() mengembalikkan sebuah promise baru yang selesai dengan response teks penuh
    // ketika dimuat
    return response.text();
  })

  .then(function (text) {
    // ...dan di sini isi dari file remote
    alert(text); // {"name": "iliakan", isAdmin: true}
  });

Di sana juga ada method response.json() yang membaca data remote dan parsing sebagai JSON. Pada kasus kita lebih sesuai, jadi mari ganti dengan itu.

.then(function(text) { // ???and here???s the content of the remote file alert(text); // {???name???: ???iliakan???, ???isAdmin???: true} });

Kita juga akan menggunakan arrow functions untuk keringkasan:

```js run
// sama seperti di atas, tetapi response.json() parsing konten remote sebagai JSON
fetch("/article/promise-chaining/user.json")
  .then((response) => response.json())
  .then((user) => alert(user.name)); // iliakan, mendapatkan user name

Sekarang mari lakukan sesuatu dengan memuat pengguna.

Sebagai contoh, kita dapat membuat satu atau lebih permintaan ke GitHub, muat profil pengguna dan tunjukkan avatarnya:

// Buat permintaan ke user.json
fetch("/article/promise-chaining/user.json")
  // Muat sebagai json
  .then((response) => response.json())
  // Buat permintaan ke GitHub
  .then((user) => fetch(`https://api.github.com/users/${user.name}`))
  // Muat response sebagai json
  .then((response) => response.json())
  // Tunjukkan gambar avatar (githubUser.avatar_url) untuk 3 detik (mungkin hidupkan itu)
  .then((githubUser) => {
    let img = document.createElement("img");
    img.src = githubUser.avatar_url;
    img.className = "promise-avatar-example";
    document.body.append(img);

    setTimeout(() => img.remove(), 3000); // (*)
  });

Kodenya bekerja, lihat komentar tentang detail. Meskipun, di sana ada potensi masalah di dalamnya, kesalahan umum dari mereka yang mulai menggunakan promise.

Lihat pada baris (*): bagaimana kita dapat melakukan sesuatu setelah avatar telah muncul dan dihapus? sebagai contoh, kita ingin menunjukkan form untuk mengubah pengguna atau sesuatu yang lain. Sampai sekarang, tidak mungkin.

Untuk membuat chain bisa diperpanjang, kita butuh untuk mengembalikkan sebuah promise yang selesai ketika avatar selesai muncul.

Seperti ini:

fetch('/article/promise-chaining/user.json')
  .then(response => response.json())
  .then(user => fetch(`https://api.github.com/users/${user.name}`))
  .then(response => response.json())
  .then(githubUser => new Promise(function(resolve, reject) { // (*)
    let img = document.createElement('img');
    img.src = githubUser.avatar_url;
    img.className = "promise-avatar-example";
    document.body.append(img);

    setTimeout(() => {
      img.remove();
      resolve(githubUser); // (**)
    }, 3000);
  }))
  // terpicu setelah 3 detik
  .then(githubUser => alert(`Finished showing ${githubUser.name}`));

Itu adalah, handler .then pada baris (*) sekarang mengembalikkan new Promise, yang menjadi mengendap hanya setelah pemanggilan resolve(githubUser) dalam setTimeout (**).

.then selanjutnya di dalam chain akan menunggu untuk itu.

Seperti peraturan yang bagus, sebuah aksi asynchronous harus selalu mengembalikkan sebuah promise.

Itu membuat kemungkinan untuk rencana aksi setelahnya. Bahkan jika kita tidak berencana memperpanjang chain sekarang, kita mungkin membutuhkannya nanti.

Akhinya, kita dapat memecah kodenya ke dalam function yang dapat digunakan kembali:

function loadJson(url) {
  return fetch(url).then((response) => response.json());
}

function loadGithubUser(name) {
  return fetch(`https://api.github.com/users/${name}`).then((response) =>
    response.json()
  );
}

function showAvatar(githubUser) {
  return new Promise(function (resolve, reject) {
    let img = document.createElement("img");
    img.src = githubUser.avatar_url;
    img.className = "promise-avatar-example";
    document.body.append(img);

    setTimeout(() => {
      img.remove();
      resolve(githubUser);
    }, 3000);
  });
}

// Gunakan mereka:
loadJson("/article/promise-chaining/user.json")
  .then((user) => loadGithubUser(user.name))
  .then(showAvatar)
  .then((githubUser) => alert(`Finished showing ${githubUser.name}`));
// ...

Ringkasan

Jika ada handler .then (atau catch/finally, tidak masalah) mengembalikkan sebuah promise, chain sisanya akan menunggu sampai mengendap. Saat itu terjadi, hasilnya (atau error) diteruskan lebih jauh.

Di sini gambar penuhnya:

Tugas

Apakah potongan kode ini sama? Dengan kata lain, apakah mereka berperilaku sama dalam situasai apapun, untuk setiap handler functions?

promise.then(f1).catch(f2);

Versus:

promise.then(f1, f2);

Jawaban singkatnya adalah: tidak, mereka tidak sama:

Perbedaannya adalah bahwa jika terjadi sebuah error di dalam f1, kemudian ditangani oleh .catch disini:

promise.then(f1).catch(f2);

???Tetapi bukan disini:

promise.then(f1, f2);

Itulah kenapa sebuah error diturunkan ke chain, dan didalam bagian kode kedua disana tidak ada chain dibawah f1.

Dengan kata lain, .then meneruskan result/error ke .then/catch selanjutnya. Jadi pada contoh pertama, ada sebuah catch di bawah, dan yang kedua ??? disana tidak ada, jadi error tidak ditangani.

Peta tutorial