Bab 6: Lain-Lain

3 menit baca

Topik lanjutan yang melengkapi pemahaman browser: mengamati perubahan DOM, mengelola seleksi teks, dan memahami event loop.


6.1 Mutation Observer

💡Analogi

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

javascript
// 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()

javascript
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

javascript
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

javascript
// 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

javascript
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()

javascript
// 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
⚠️Jebakan!
javascript
// 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

javascript
// 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 observer

6.2 Selection dan Range

💡Analogi

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

javascript
// 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

javascript
// 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

javascript
// 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

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 5

Event Selection

javascript
// 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

javascript
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

javascript
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
});
⚠️Jebakan!
javascript
// 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

javascript
// 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 array

6.3 Event Loop: Microtask dan Macrotask

💡Analogi

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

MacrotaskMicrotask
setTimeout/setIntervalPromise.then/catch/finally
Event handler (click, etc)queueMicrotask()
Script loadingMutationObserver callback
I/O operationsasync/await (setelah await)

Contoh: Urutan Eksekusi

javascript
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?

javascript
// 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
javascript
// 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 biru

queueMicrotask()

javascript
// 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

javascript
// ❌ 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

javascript
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 2

Penjelasan:

  1. "Start" dan "End" — synchronous, langsung jalan
  2. Promise 1 dan Promise 3 — microtask, jalan setelah sync selesai
  3. Promise 2 — microtask dari Promise 1 (masuk queue setelah Promise 1 jalan)
  4. Timeout 1 dan 2 — macrotask, jalan setelah semua microtask selesai

Rendering dan Event Loop

javascript
// 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)
⚠️Jebakan!
javascript
// 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

javascript
// 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 → macrotask

Ringkasan Bab 6

Sub-babInti
Mutation Observer"CCTV" untuk DOM — amati perubahan child, atribut, teks
Selection & RangeManipulasi teks yang dipilih user, custom highlight
Event LoopSync → 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:

BabTopik
1. DocumentDOM tree, navigasi, searching, modifikasi, style, ukuran, koordinat
2. EventsaddEventListener, bubbling, delegation, preventDefault, custom events
3. UI EventsMouse, pointer, keyboard, scroll
4. FormsProperti form, focus/blur, input/change, submit
5. LoadingDOMContentLoaded, async/defer, resource onload/onerror
6. MiscellaneousMutationObserver, 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.