Asynchronous Javascript 2 - Callback


Definisi paling sederhana dan cukup dari fungsi Callback adalah “Sebuah fungsi yang dijadikan argumen untuk fungsi yang lain, yang nantinya akan dipanggil oleh fungsi tersebut untuk menyelesaikan suatu tugas”.

function makeCoffee(callback) {
  console.log("brewing delicious coffee...");
  callback();
}

function getCoffee() {
  console.log("Here is your coffee ☕");
}

makeCoffee(getCoffee);

Pada kode diatas, fungsi getCoffee dijadikan argumen untuk fungsi makeCoffee yang akan dipanggil setelah proses tertentu. getCoffee disini adalah sebuah callback.

Jika sudah cukup lama belajar Javascript, secara tidak sadar kamu pasti sering bersinggungan dengan callback. Saat menggunakan timeout, atau operasi dengan array.

const arr = ["halo", "darkness"];
function callback(str) {
  return str.toUpperCase();
}
const capitalized = arr.map(callback);

// setTimeout
function callback2() {
  console.log(capitalized);
}
setTimeout(callback2, 100); // ['HALO', 'DARKNESS']

Synchronous vs Asynchronous Callback

Callback bisa dipanggil secara synchronous maupun asynchronous , dan membedakan keduanya penting ketika menganalisa side-effects (akibat sebuah proses yang merubah suatu keadaan, seperti merubah value variabel) dari suatu kode yang dieksekusi. Perhatikan kode berikut

let num = 1;

function tambah(callback) {
  callback()
}

tambah(() => {
  num = 2;
});

console.log(num);

apabila Callback dari suatuFungsi dipanggil secara synchronous, maka output dari num adalah 2 tapi jika asynchronous, maka hasilnya adalah 1, karena variabel num belum memiliki nilai yang baru.

Callback untuk Event listener

Event listener yang biasa digunakan untuk “mendengar” segala hal yang terjadi di DOM, juga menerima suatu Callback untuk dipanggil ketika suatu “Event” berjalan. Kode dibawah menampilkan Callback yang dipanggil setiap cursor mouse bergerak.

window.addEventListener('pointermove', (event) => {
  const container = document.querySelector('#data');

  container.innerText = 'Koordinat X: ' + event.clientX + ' • Koordinat Y: ' + event.clientY;
});

Callback untuk HTTP request

Salah satu proses yang tidak tentu waktu eksekusinnya adalah HTTP request, yaitu proses interaksi dengan server yang dilakukan oleh client (misal browser) untuk mendapatkan atau mengirim data.

Berikut adalah contoh kode penggunaan AJAX untuk melakukan HTTP request ke fake API service jsonplaceholder.

function getData(url, callback) {
  const request = new XMLHttpRequest();

  request.addEventListener("load", () => {
    if (request.status >= 200 && request.status < 300) {
      callback(undefined, request.responseText);
    } else {
      callback("Error", undefined);
    }
  });

  request.open("GET", url);
  request.send();
}

getData("https://jsonplaceholder.typicode.com/users", (err, data) => {
  if (err) {
    console.log(err);
  } else {
    document.getElementById("display").innerHTML = data
  }
});

Pada kode di atas, fungsi getData akan melakukan permintaan ke API untuk mendapatkan data pengguna. Fungsi getData memiliki dua parameter, yaitu url untuk URL API dan callback yang akan dipanggil dengan hasil data sukses ataupun error.

  • Parameter url adalah URL endpoint yang akan diakses.
  • Parameter callback adalah fungsi yang akan dieksekusi setelah permintaan selesai. Callback ini memiliki dua argumen: err untuk error (jika ada), dan data untuk data yang diterima dari server.

Penggunaan callback dalam menangani operasi asynchronous memiliki kelemahan yaitu potensi terbentuknya callback hell, di mana callback bertumpuk terlalu dalam sehingga kode menjadi sulit dibaca dan dikelola. Untuk mengatasi ini, JavaScript memperkenalkan Promise dan async/await.

Callback hell

Neraka callback adalah suatu kondisi dimana Callback memiliki banyak callback lain didalamnya, saling membungkus satu sama lain, sehingga kode yang ada jadi sulit dibaca dan rentan terhadap bug.

Menggunakan contoh AJAX sebelumnya, bisa dibuat skenario dimana data yang diminta dari server saling terkait sehingga menciptakan neraka callback.

Misalkan untuk mendapatkan data pengguna yang membuat sebuah post dimana pada post tersebut terdapat komentar berisi ujaran kebencian. Maka, tahapan yang perlu dilalui untuk mendapatkan data pengguna tersebut adalah:

  • mendapatkan data komentar berisi ujaran kebencian
  • mencari data post dengan komentar tersebut
  • mendapatkan data pengguna yang membuat post

Berikut adalah kode untuk melakukan tahapan diatas:

function getData(url, callback) {
  const request = new XMLHttpRequest();

  request.addEventListener("load", () => {
    if (request.status >= 200 && request.status < 300) {
      const data = JSON.parse(request.responseText);
      callback(undefined, data);
    } else {
      callback("Error", undefined);
    }
  });

  request.open("GET", url);
  request.send();
}

const baseUrl = "https://jsonplaceholder.typicode.com";

// mendapatkan data komentar
getData(baseUrl + "/comments/1", (err, data) => {
  if (err) {
    console.log(err);
  } else {
    const postId = data.postId;
    // mendapatkan data post spesifik
    getData(baseUrl + "/posts/" + postId, (err, data) => {
      if (err) {
        console.log(err);
      } else {
        const userId = data.userId;
        // mendapatkan data user
        getData(baseUrl + "/users/" + userId, (err, data) => {
          if (err) {
            console.log(err);
          } else {
            document.getElementById("display").innerHTML = JSON.stringify(data, null, 4);
          }
        });
      }
    });
  }
});

Perhatikan penggunaan fungsi getData yang saling membungkus pada kode diatas. Sekilas melihat saja cukup sulit untuk menangkap proses permintaan dan penampilan data. Kode diatas padahal masih cukup sederhana karena tidak ada manipulasi data atau pengangan error yang serius.

Bayangkan apabila proses permintaan data dari server bukan hanya 3, tapi 5 atau 7, akan sesulit apa me-menej kode nantinya. Maka kondisi ini disebut sebagai neraka callback atau pyramid of doom, piramida kiamat, karena pada sisi kiri kode akan terbentuk piramid yang makin tinggi makin mengerikan.

Bonus: Callback untuk pemrograman yang lebih dinamis

Selain berguna untuk berhadapan dengan situasi asynchronous, callback juga berguna untuk membuat abstraksi yang lebih dinamis. Perhatikan kode berikut

function validateString(str, rules) {
  // membersihkan string
  const cleanString = str.trim();

  // cek apakah string kosong
  if (rules === "required") {
    if (cleanString.length === 0) {
      return false;
    }
  }

  // cek apakah string sepenuhnya alphabet
  if (rules === "alphabet") {
    if (/^[A-Za-z ]+$/.test(cleanString)) {
      return false;
    }
  }

  // cek maksimal karakter
  if (rules === "max") {
    if (cleanString.length > 10) {
      return false;
    }
  }

  // jika semua validasi oke
  return true;
}

Kode diatas adalah fungsi sederhana untuk melakukan validasi terhadap karakter pada suatu string. Secara fungsionalitas, fungsi validateString sudah oke, ia bisa memvalidasi sesuai yang diinginkan. Namun, bagaimana jika inging ditambahkan validasi yang lainnya? jika makin banyak kondisi yang ditambahkan, tentu fungsi akan semakin besar dan monoton.

Dengan callback, kode diatas dapat dibuat jadi lebih dinamis dan meningkatkan readability nya. Lihat modifikasi kode dibawah:

function validateString(str, validator) {
  // membersihkan string
  const cleanString = str.trim();

  // melakukan validasi
  return validator(cleanString);
}

function isRequired(str) {
  return str.trim().length > 0;
}

function isAlphabetic(str) {
  return /^[A-Za-z ]+$/.test(str);
}

function maxLength(str, maxLength) {
  return str.length <= maxLength;
}

// penggunaan
validateString("Hello", isRequired);
validateString("Hello", isAlphabetic);
validateString("Hello", (str) => maxLength(str, 5));

Referensi