Bab 6: Lain-Lain
Topik lanjutan yang melengkapi pemahaman browser: mengamati perubahan DOM, mengelola seleksi teks, dan memahami event loop.
6.1 Mutation Observer
Mutation Observer itu kayak CCTV untuk DOM — dia mengawasi perubahan pada elemen (child ditambah/dihapus, atribut berubah, teks berubah) dan memberitahu kamu saat ada perubahan.
Kenapa Perlu?
Kadang kamu perlu tahu saat:
- Library pihak ketiga mengubah DOM
- Konten dinamis ditambahkan (infinite scroll, ads)
- Atribut berubah (class ditambah/dihapus)
- Kamu membuat plugin yang harus bereaksi terhadap perubahan DOM
Cara Pakai
// 1. Buat observer dengan callback
const observer = new MutationObserver(function(mutations) {
for (const mutation of mutations) {
console.log("Perubahan terdeteksi:", mutation.type);
}
});
// 2. Mulai mengamati elemen
const target = document.getElementById("content");
observer.observe(target, {
childList: true, // Amati penambahan/penghapusan child
subtree: true, // Amati juga semua descendant
attributes: true, // Amati perubahan atribut
characterData: true // Amati perubahan teks
});
// 3. Berhenti mengamati
observer.disconnect();Opsi observe()
observer.observe(target, {
childList: true, // Child ditambah/dihapus
attributes: true, // Atribut berubah
characterData: true, // Teks node berubah
subtree: true, // Amati seluruh subtree (bukan hanya direct children)
attributeFilter: ["class", "style"], // Hanya atribut tertentu
attributeOldValue: true, // Simpan nilai atribut lama
characterDataOldValue: true // Simpan teks lama
});Objek MutationRecord
const observer = new MutationObserver(function(mutations) {
for (const mutation of mutations) {
switch (mutation.type) {
case "childList":
console.log("Child ditambah:", mutation.addedNodes);
console.log("Child dihapus:", mutation.removedNodes);
break;
case "attributes":
console.log("Atribut berubah:", mutation.attributeName);
console.log("Nilai lama:", mutation.oldValue);
break;
case "characterData":
console.log("Teks berubah:", mutation.target.data);
break;
}
}
});Contoh: Auto-Highlight Code Blocks
// Otomatis highlight <pre><code> yang baru ditambahkan
const observer = new MutationObserver(function(mutations) {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.nodeType !== 1) continue; // Skip non-element
// Cek apakah node itu code block
if (node.matches("pre > code")) {
highlightCode(node);
}
// Atau cek child-nya
node.querySelectorAll?.("pre > code").forEach(highlightCode);
}
}
});
observer.observe(document.body, { childList: true, subtree: true });Contoh: Deteksi Perubahan Class
const btn = document.getElementById("my-btn");
const observer = new MutationObserver(function(mutations) {
for (const mutation of mutations) {
if (mutation.attributeName === "class") {
const newClasses = btn.className;
console.log("Class berubah jadi:", newClasses);
if (btn.classList.contains("active")) {
console.log("Button sekarang aktif!");
}
}
}
});
observer.observe(btn, {
attributes: true,
attributeFilter: ["class"],
attributeOldValue: true
});takeRecords()
// Ambil mutations yang belum diproses (sebelum callback dipanggil)
const pending = observer.takeRecords();
// Berguna sebelum disconnect — pastikan tidak ada yang terlewat
observer.disconnect();
// Process pending mutations manually if needed// 1. Callback dipanggil ASYNCHRONOUS (setelah semua perubahan selesai)
// Bukan real-time per perubahan — di-batch
// 2. Hati-hati infinite loop!
// Kalau callback mengubah DOM yang sedang diamati → bisa loop
const observer = new MutationObserver(function(mutations) {
// ❌ Ini bisa infinite loop!
// target.innerHTML += "baru";
});
// 3. Performance: jangan observe terlalu banyak
// observe(document.body, { subtree: true, childList: true, attributes: true })
// Ini berat! Hanya observe yang benar-benar dibutuhkan
// 4. disconnect() HARUS dipanggil saat tidak dibutuhkan lagi
// Kalau tidak → memory leak🎯 Challenge
// Buat "DOM change logger":
// 1. Observe <div id="playground">
// 2. Setiap perubahan (child, atribut, teks) → log ke <ul id="log">
// 3. Format log: "[waktu] type: detail"
// 4. Buat beberapa tombol untuk test:
// - "Add Child" → tambah <p> baru
// - "Change Attr" → toggle class pada playground
// - "Change Text" → ubah teks child pertama
// 5. Tombol "Stop" → disconnect observer6.2 Selection dan Range
Pernah select/highlight teks di halaman web? Itu namanya Selection. Range adalah "penanda" yang menentukan dari mana sampai mana teks yang dipilih — kayak highlighter marker di buku.
Range — Menandai Bagian DOM
// Buat range
const range = new Range();
// Set batas range
const p = document.querySelector("p");
range.setStart(p.firstChild, 2); // Mulai dari karakter ke-2 di text node
range.setEnd(p.firstChild, 8); // Sampai karakter ke-8
// Method lain untuk set range
range.setStartBefore(node); // Sebelum node
range.setStartAfter(node); // Setelah node
range.setEndBefore(node); // Sebelum node
range.setEndAfter(node); // Setelah node
range.selectNode(node); // Seluruh node
range.selectNodeContents(node); // Isi node (tanpa node itu sendiri)Manipulasi Range
// Hapus konten dalam range
range.deleteContents();
// Extract (hapus dan return sebagai fragment)
const fragment = range.extractContents();
// Clone (copy tanpa hapus)
const clone = range.cloneContents();
// Insert node di awal range
const bold = document.createElement("b");
range.insertNode(bold);
// Bungkus konten range dengan elemen
const span = document.createElement("span");
span.style.background = "yellow";
range.surroundContents(span); // Highlight!Selection — Apa yang User Pilih
// Ambil selection saat ini
const selection = window.getSelection();
console.log(selection.toString()); // Teks yang dipilih
console.log(selection.rangeCount); // Jumlah range (biasanya 1)
console.log(selection.getRangeAt(0)); // Range pertama
// Cek apakah ada yang dipilih
if (!selection.isCollapsed) {
console.log("Ada teks yang dipilih:", selection.toString());
}Mengatur Selection via JavaScript
// Pilih semua teks di elemen
const elem = document.getElementById("content");
const range = new Range();
range.selectNodeContents(elem);
const selection = window.getSelection();
selection.removeAllRanges(); // Hapus selection lama
selection.addRange(range); // Set selection baru
// Shortcut: select semua isi input
const input = document.querySelector("input");
input.select(); // Select semua teks di input
// Select sebagian teks di input
input.setSelectionRange(2, 5); // Karakter 2 sampai 5Event Selection
// Event saat selection berubah
document.addEventListener("selectionchange", function() {
const selection = window.getSelection();
console.log("Selection:", selection.toString());
});
// Untuk input/textarea
input.addEventListener("select", function() {
console.log("Selected:", this.value.substring(this.selectionStart, this.selectionEnd));
});Contoh: Custom Highlight
document.addEventListener("mouseup", function() {
const selection = window.getSelection();
if (selection.isCollapsed) return; // Tidak ada yang dipilih
const range = selection.getRangeAt(0);
// Bungkus dengan <mark>
const mark = document.createElement("mark");
try {
range.surroundContents(mark);
} catch (e) {
// surroundContents gagal kalau range melintasi batas elemen
console.log("Tidak bisa highlight (melintasi elemen)");
}
selection.removeAllRanges(); // Hapus selection
});Contoh: Copy dengan Format Custom
document.addEventListener("copy", function(event) {
const selection = window.getSelection();
const text = selection.toString();
// Tambahkan sumber di akhir
const modified = text + "\n\nSumber: " + location.href;
event.clipboardData.setData("text/plain", modified);
event.preventDefault(); // Cegah copy default
});// 1. surroundContents() GAGAL kalau range melintasi batas elemen
// <p>Halo <b>du|nia</b> se|mua</p> → Error!
// Solusi: pakai extractContents + insertNode manual
// 2. Selection bisa hilang saat DOM berubah
// Simpan range sebelum manipulasi DOM
// 3. Di mobile, selection behavior berbeda
// Touch-and-hold untuk select, bukan drag
// 4. user-select: none di CSS → elemen tidak bisa di-select🎯 Challenge
// Buat "text annotator":
// 1. User select teks di <div id="article">
// 2. Muncul tombol "Highlight" di dekat selection
// 3. Klik Highlight → teks dibungkus <mark> (kuning)
// 4. Klik mark yang sudah ada → hapus highlight (unwrap)
// 5. Simpan semua highlight positions di array6.3 Event Loop: Microtask dan Macrotask
JavaScript itu single-threaded — cuma bisa kerjakan satu hal pada satu waktu. Event loop itu kayak antrian di bank:
- Call stack = teller yang sedang melayani (satu orang pada satu waktu)
- Macrotask queue = antrian utama (setTimeout, event handler, dll)
- Microtask queue = antrian prioritas/VIP (Promise.then, queueMicrotask)
Aturan: VIP (microtask) SELALU dilayani dulu sebelum antrian biasa (macrotask).
Event Loop Cycle
1. Ambil satu macrotask dari queue → jalankan
2. Jalankan SEMUA microtask yang ada (sampai habis)
3. Render (update tampilan jika perlu)
4. Kembali ke langkah 1
Macrotask vs Microtask
| Macrotask | Microtask |
|---|---|
| setTimeout/setInterval | Promise.then/catch/finally |
| Event handler (click, etc) | queueMicrotask() |
| Script loading | MutationObserver callback |
| I/O operations | async/await (setelah await) |
Contoh: Urutan Eksekusi
console.log("1. Synchronous");
setTimeout(() => {
console.log("4. Macrotask (setTimeout)");
}, 0);
Promise.resolve().then(() => {
console.log("3. Microtask (Promise)");
});
console.log("2. Synchronous lagi");
// Output:
// 1. Synchronous
// 2. Synchronous lagi
// 3. Microtask (Promise)
// 4. Macrotask (setTimeout)Kenapa Urutan Ini Penting?
// Microtask jalan SEBELUM render!
document.body.style.background = "red";
Promise.resolve().then(() => {
// Ini jalan SEBELUM browser render
document.body.style.background = "blue";
});
// User TIDAK PERNAH melihat background merah!
// Karena microtask selesai sebelum render// Macrotask jalan SETELAH render
document.body.style.background = "red";
setTimeout(() => {
// Ini jalan SETELAH browser render
document.body.style.background = "blue";
}, 0);
// User MELIHAT merah sebentar, lalu berubah ke biruqueueMicrotask()
// Cara eksplisit menambah microtask
queueMicrotask(() => {
console.log("Ini microtask");
});
// Berguna untuk: menunda eksekusi tapi tetap sebelum render
// Contoh: batch DOM updates
let pendingUpdates = [];
function scheduleUpdate(update) {
pendingUpdates.push(update);
if (pendingUpdates.length === 1) {
queueMicrotask(() => {
// Process semua updates sekaligus
const updates = pendingUpdates;
pendingUpdates = [];
updates.forEach(fn => fn());
});
}
}Contoh: Unblocking UI dengan Macrotask
// ❌ BURUK — blocking UI (halaman freeze)
function heavyTask() {
for (let i = 0; i < 1000000000; i++) {
// Kalkulasi berat...
}
}
// ✅ BAGUS — pecah jadi chunk dengan setTimeout
function heavyTaskChunked(data, chunkSize = 1000) {
let i = 0;
function processChunk() {
const end = Math.min(i + chunkSize, data.length);
while (i < end) {
// Process satu item
processItem(data[i]);
i++;
}
// Update progress
updateProgress(i / data.length * 100);
if (i < data.length) {
setTimeout(processChunk, 0); // Yield ke browser (bisa render)
} else {
console.log("Selesai!");
}
}
processChunk();
}Contoh: Promise Chain dan Event Loop
console.log("Start");
setTimeout(() => console.log("Timeout 1"), 0);
setTimeout(() => console.log("Timeout 2"), 0);
Promise.resolve()
.then(() => {
console.log("Promise 1");
return Promise.resolve();
})
.then(() => console.log("Promise 2"));
Promise.resolve().then(() => console.log("Promise 3"));
console.log("End");
// Output:
// Start
// End
// Promise 1
// Promise 3
// Promise 2
// Timeout 1
// Timeout 2Penjelasan:
- "Start" dan "End" — synchronous, langsung jalan
- Promise 1 dan Promise 3 — microtask, jalan setelah sync selesai
- Promise 2 — microtask dari Promise 1 (masuk queue setelah Promise 1 jalan)
- Timeout 1 dan 2 — macrotask, jalan setelah semua microtask selesai
Rendering dan Event Loop
// Browser render ANTARA macrotask (setelah microtask selesai)
// Tapi browser bisa SKIP render kalau tidak ada perubahan visual
// requestAnimationFrame — dijamin jalan SEBELUM render berikutnya
requestAnimationFrame(() => {
// Update animasi di sini
elem.style.transform = `translateX(${x}px)`;
});
// Urutan dalam satu frame:
// 1. Macrotask
// 2. Semua microtask
// 3. requestAnimationFrame callbacks
// 4. Render (paint)// 1. Microtask yang membuat microtask baru → SEMUA diproses sebelum render
Promise.resolve().then(function loop() {
// ❌ BAHAYA! Infinite microtask → browser FREEZE (tidak pernah render)
Promise.resolve().then(loop);
});
// 2. setTimeout(fn, 0) BUKAN benar-benar 0ms
// Minimum delay: ~4ms (browser spec)
// Dan harus menunggu macrotask sebelumnya selesai
// 3. async/await: kode setelah await = microtask
async function foo() {
console.log("A"); // Synchronous
await something;
console.log("B"); // Microtask (seperti .then())
}
// 4. Event handler dari user action = macrotask
// Event handler dari dispatchEvent = synchronous!🎯 Challenge
// Prediksi output dari kode ini (tulis jawaban sebelum jalankan):
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
requestAnimationFrame(() => console.log("4"));
queueMicrotask(() => console.log("5"));
Promise.resolve().then(() => {
console.log("6");
queueMicrotask(() => console.log("7"));
});
console.log("8");
// Tulis prediksi urutan output-mu, lalu jalankan di browser untuk verifikasi
// Hint: sync → microtask (semua) → rAF → macrotaskRingkasan Bab 6
| Sub-bab | Inti |
|---|---|
| Mutation Observer | "CCTV" untuk DOM — amati perubahan child, atribut, teks |
| Selection & Range | Manipulasi teks yang dipilih user, custom highlight |
| Event Loop | Sync → Microtask (semua) → Render → Macrotask → ulang |
Prinsip utama:
- MutationObserver untuk react terhadap perubahan DOM (bukan polling)
- Selection API untuk fitur highlight, annotasi, custom copy
- Event loop: microtask (Promise) selalu sebelum macrotask (setTimeout)
- Jangan block main thread — pecah heavy task dengan setTimeout/requestAnimationFrame
queueMicrotask()untuk eksekusi sebelum render tapi setelah sync
🎉 Part 2 Selesai!
Recap Part 2: Browser — Document, Events, Interfaces:
| Bab | Topik |
|---|---|
| 1. Document | DOM tree, navigasi, searching, modifikasi, style, ukuran, koordinat |
| 2. Events | addEventListener, bubbling, delegation, preventDefault, custom events |
| 3. UI Events | Mouse, pointer, keyboard, scroll |
| 4. Forms | Properti form, focus/blur, input/change, submit |
| 5. Loading | DOMContentLoaded, async/defer, resource onload/onerror |
| 6. Miscellaneous | MutationObserver, Selection/Range, Event Loop |
Next: Part 3 — Additional Articles (Network requests, Storage, Animation, Web Components, Regex)
Sudah paham materi ini?
Tandai sebagai selesai untuk melacak progress-mu.