Node.js dari awal dirancang berkelindan dengan kejadian: koneksi masuk, berkas selesai dibaca, timer berakhir — semuanya adalah event. Pola yang menyatukan semuanya bernama publish-subscribe, dan implementasinya di Node.js adalah class EventEmitter dari module node:events. Stream, server HTTP, hingga koneksi database memakai class yang sama ini sebagai dasar notifikasinya, jadi memahaminya sekali berarti memahami bahasa notifikasi di seluruh ekosistem. Pada chapter ini kita membangun emitter sendiri dari nol sampai class turunan yang siap dipakai aplikasi nyata.

Mendaftar dan Memicu Event #

Alur kerja EventEmitter punya dua sisi. Sisi pemasang memakai method on(namaEvent, listener) untuk mendaftar function yang akan dipanggil; sisi pemicu memakai emit(namaEvent, ...data) untuk mengumumkan kejadian beserta datanya. Berkas event-dasar.mjs memperlihatkan kedua sisi sekaligus satu perilaku penting tentang kata kunci this:

js
import { EventEmitter } from "node:events";

const penerbit = new EventEmitter();

penerbit.on("sapa", function (nama) {
  console.log(`Halo, ${nama}!`);
  console.log("this === penerbit:", this === penerbit);
});

penerbit.on("sapa", (nama) => {
  console.log(`Salam juga untuk ${nama}.`);
});

penerbit.emit("sapa", "Budi");

Output yang Dihasilkan:

text
Halo, Budi!
this === penerbit: true
Salam juga untuk Budi.

Mari kita telaah baris-baris kode di atas:

  1. Baris 5 (penerbit.on("sapa", function ...)): Listener pertama didaftarkan memakai function declaration biasa. Di dalam function tersebut, this terikat ke instance emitter — perilaku yang dipakai listener lama untuk mengakses emitter yang memanggilnya, misalnya this.emit(...) untuk memicu event lain.
  2. Baris 10 (penerbit.on("sapa", (nama) => ...)): Listener kedua memakai arrow function. Arrow function tidak memiliki this sendiri, sehingga nilainya diambil dari lingkup sekitar — bukan dari emitter. Perbedaan ini penting saat membaca kode library lama, tetapi untuk listener yang hanya memakai parameternya, kedua gaya tersebut setara.
  3. Baris 14 (penerbit.emit("sapa", "Budi")): Pemicuan menjalankan kedua listener secara berurutan sesuai urutan pendaftaran, dan argumen setelah nama event diteruskan apa adanya ke tiap listener.

once, Alias, dan Menghitung Listener #

Kebutuhan nyata tidak berhenti di pasang-pasang: ada listener yang cukup berbunyi sekali, ada saat kita perlu melepas listener, dan ada saat kita ingin memeriksa dulu ada berapa listener yang aktif. Tabel berikut merangkum method yang bertugas:

MethodPeran
on(nama, listener)Mendaftar listener tetap
once(nama, listener)Mendaftar listener sekali pakai
off(nama, listener)Melepas satu listener
emit(nama, ...data)Memicu event beserta datanya
listenerCount(nama)Menghitung listener aktif untuk satu event
eventNames()Mendaftar semua event yang masih punya listener

addListener() dan removeListener() adalah nama panggilan lama untuk on() dan off() — keduanya masih sah dan sering dijumpai di kode warisan. Berkas event-sekali.mjs memakai nama lama dan baru tersebut secara bergantian agar keduanya familiar:

js
import { EventEmitter } from "node:events";

const alarm = new EventEmitter();

function padaPesan(pesan) {
  console.log("diterima:", pesan);
}

alarm.once("berbunyi", () => console.log("alarm berbunyi (sekali)"));
alarm.addListener("berbunyi", padaPesan);

console.log("listener awal :", alarm.listenerCount("berbunyi"));

alarm.emit("berbunyi", "pagi");
alarm.emit("berbunyi", "siang");

console.log("listener akhir:", alarm.listenerCount("berbunyi"));
console.log("daftar event  :", alarm.eventNames());

alarm.removeListener("berbunyi", padaPesan);
alarm.emit("berbunyi", "sore");
console.log("setelah lepas :", alarm.listenerCount("berbunyi"));

Output yang Dihasilkan:

text
listener awal : 2
alarm berbunyi (sekali)
diterima: pagi
diterima: siang
listener akhir: 1
daftar event  : [ 'berbunyi' ]
setelah lepas : 0

Urutan outputnya menyimpan dua pelajaran. Pertama, listener awal bernilai 2 karena dua listener terdaftar, tetapi setelah emit() pertama nilai “akhir”-nya tinggal 1 — listener dari once() melepas dirinya sendiri begitu selesai dijalankan, itulah gunanya nama metodenya. Kedua, emit("berbunyi", "siang") hanya mencetak satu baris diterima: siang karena listener sekali pakai sudah tidak aktif. Baris terakhir membuktikan removeListener() bekerja: setelah padaPesan dilepas, emit("berbunyi", "sore") tidak mencetak apa pun dan jumlah listener menjadi 0.

Konvensi Event error #

Di antara semua nama event, "error" menempati posisi khusus. Ketika emit("error", error) dipanggil dan tidak ada satu pun listener yang terdaftar, EventEmitter tidak diam saja — object error tersebut dilempar sebagai exception tepat di baris emit()-nya, menghentikan proses bila tidak ditangkap. Berkas error-event.mjs memperlihatkan kedua keadaan itu:

js
import { EventEmitter } from "node:events";

const sensor = new EventEmitter();

sensor.on("error", (error) => {
  console.log("tertangani:", error.message);
});

sensor.emit("error", new Error("suhu melebihi batas"));

sensor.removeAllListeners("error");

try {
  sensor.emit("error", new Error("suhu kritis"));
} catch (error) {
  console.log("terlempar:", error.message);
}

Output yang Dihasilkan:

text
tertangani: suhu melebihi batas
terlempar: suhu kritis

Pemanggilan emit() pertama aman karena listener "error" sudah terpasang — error sampai ke listener sebagai argumen biasa. Setelah removeAllListeners("error") melepas satu-satunya listener, pemanggilan kedua melempar exception yang kita tangkap dengan try/catch. Konvensi ini menjaga error tidak pernah menguap tanpa kabar: komponen pengirim cukup emit("error", ...), dan komponen pengguna wajib memasang listener untuk menanganinya.

Class Turunan EventEmitter #

Pada aplikasi nyata, kita jarang memakai EventEmitter telanjang; pola yang lebih lazim adalah membuat class sendiri yang mewarisi EventEmitter, lalu memicu event dari dalam method-nya. Konsumen class tersebut tinggal memasang listener tanpa perlu tahu detail internalnya. Berkas daftar-tugas.mjs menerapkan pola itu untuk daftar tugas yang mengumumkan setiap penambahan:

js
import { EventEmitter } from "node:events";

class DaftarTugas extends EventEmitter {
  constructor() {
    super();
    this.daftar = [];
  }

  tambah(nama) {
    const tugas = { nama, urutan: this.daftar.length + 1 };
    this.daftar.push(tugas);
    this.emit("ditambahkan", tugas);
    if (this.daftar.length >= 3) {
      this.emit("penuh", this.daftar.length);
    }
  }
}

const daftarSaya = new DaftarTugas();

daftarSaya.on("ditambahkan", (tugas) => {
  console.log(`+ tugas #${tugas.urutan}: ${tugas.nama}`);
});

daftarSaya.on("penuh", (jumlah) => {
  console.log(`mencapai ${jumlah} tugas, waktunya mengerjakan!`);
});

daftarSaya.tambah("menulis laporan");
daftarSaya.tambah("mengirim email");
daftarSaya.tambah("menyiapkan rapat");

Output yang Dihasilkan:

text
+ tugas #1: menulis laporan
+ tugas #2: mengirim email
+ tugas #3: menyiapkan rapat
mencapai 3 tugas, waktunya mengerjakan!

Mari kita telaah baris-baris kode di atas:

  1. Baris 3 (class DaftarTugas extends EventEmitter): Kata kunci extends membuat instance DaftarTugas sekaligus menjadi emitter — method on(), emit(), dan method lainnya diwarisi langsung.
  2. Baris 5 (super()): Constructor class turunan wajib memanggil constructor induk sebelum menyentuh this; tanpa super(), baris this.daftar = [] memicu ReferenceError.
  3. Baris 9-16 (tambah(nama)): Method inti menyimpan tugas ke array internal, memicu "ditambahkan" dengan object tugas sebagai data, lalu memicu "penuh" saat batas tiga tugas tercapai. Pemanggil tambah() tidak perlu tahu event apa pun — notifikasi sepenuhnya urusan internal class.
  4. Baris 21-27: Konsumen memasang dua listener untuk dua event berbeda; output terakhir membuktikan event "penuh" menyala tepat pada penambahan ketiga.

Pola class turunan inilah yang akan kita jumpai terus-menerus: http.Server, stream di node:fs, hingga object process itu sendiri memancarkan event dengan mekanisme yang sama persis. Berbicara tentang process, object global itu adalah emitter yang paling dekat dengan kita — memancarkan event exit dan beforeExit serta menyediakan argumen dan environment variable. Chapter berikutnya membedahnya tuntas.

Referensi lanjutan untuk topik chapter ini: