JavaScript
DOM Manipulation
Event delegation
เรียนรู้เทคนิค Event Delegation โดยผูก listener เดียวที่ parent แทนการผูกแยกทุก child พร้อมใช้ event.target.matches() กรอง element
ปัญหาของการผูก listener แยกทุก element
สมมติมีรายการ `<li>` 100 ตัว ถ้าต้องการให้ทุกตัว respond ต่อ click จะต้องผูก listener 100 ครั้ง นอกจากจะหนักแล้ว ยังมีปัญหาอีกอย่าง — element ที่เพิ่มเข้ามาใหม่ภายหลังจะไม่มี listener ผูกอยู่
ทุกครั้งที่เพิ่ม element ใหม่ต้องผูก listener ใหม่ทุกครั้ง
// ❌ ผูกทีละตัว — หนักและไม่รองรับ element ใหม่
const items = document.querySelectorAll("li");
items.forEach(function(item) {
item.addEventListener("click", function(e) {
console.log(e.target.textContent);
});
});
// ถ้าเพิ่ม <li> ใหม่เข้าไปทีหลัง li ใหม่จะไม่มี listenerEvent Delegation — ผูก listener เดียวที่ parent
แนวทางที่ดีกว่าคือ **Event Delegation** — ผูก listener ตัวเดียวที่ parent element แทน สิ่งที่ทำให้วิธีนี้ใช้ได้คือ **Event Bubbling** — เมื่อ event เกิดที่ child มันจะ "bubble" (ส่งต่อ) ขึ้นไปยัง parent และ ancestor ต่าง ๆ โดยอัตโนมัติ
listener ผูกที่ ul ตัวเดียว แต่ทำงานได้กับทุก li ที่คลิก
// ✅ ผูกที่ parent เดียว — รองรับทุก li รวมถึงที่เพิ่มใหม่
const list = document.querySelector("ul");
list.addEventListener("click", function(e) {
console.log(e.target.textContent);
});Event Bubbling คืออะไร
เมื่อ click เกิดที่ `<li>` — event จะ bubble ขึ้นไปตาม tree: `li` → `ul` → `body` → `html` → `document` ดังนั้น listener ที่ผูกไว้กับ `ul` จะรับ event ที่เกิดจาก `li` ด้วย
e.target และ e.currentTarget ต่างกัน — target คือจุดที่คลิก, currentTarget คือ element ที่ผูก listener
const list = document.querySelector("ul");
list.addEventListener("click", function(e) {
// e.target = element ที่ถูกคลิกจริง ๆ (เช่น li)
// e.currentTarget = element ที่ผูก listener ไว้ (คือ ul)
console.log("target:", e.target);
console.log("currentTarget:", e.currentTarget);
});กรอง element ที่ต้องการด้วย `event.target.matches()`
เพราะ event bubble ขึ้นมา ผู้ใช้อาจคลิก parent หรือ element อื่นใน list ได้ด้วย `element.matches(selector)` รับ CSS selector แล้วคืน `true` ถ้า element นั้นตรงกับ selector — ใช้กรองว่า element ที่คลิกจริง ๆ คือ `li` หรือเปล่า
matches() ให้ความแม่นยำ — handler ทำงานเฉพาะเมื่อคลิก element ที่ตรงกับ selector
const list = document.querySelector("ul");
list.addEventListener("click", function(e) {
// กรองเฉพาะ li — ไม่สนใจถ้าคลิกที่ ul โดยตรง
if (e.target.matches("li")) {
console.log("คลิก li:", e.target.textContent);
}
});// กรองด้วย class ก็ได้
list.addEventListener("click", function(e) {
if (e.target.matches("li.active")) {
console.log("คลิก li ที่มี class active");
}
});ข้อควรระวัง
- **Event ที่ไม่ bubble ใช้ delegation ไม่ได้** — `focus` และ `blur` ไม่ bubble ขึ้นมา ถ้าต้องการ delegation ให้ใช้ `focusin` และ `focusout` แทน ซึ่ง bubble ได้
- **matches() ต้องระบุ selector ให้ถูก** — ถ้า element ซ้อนกัน เช่น `<li><span>ข้อความ</span></li>` และคลิกที่ span จะได้ `e.target` เป็น span ไม่ใช่ li — ให้ใช้ `e.target.closest('li')` แทน
- **stopPropagation หยุด bubble** — ถ้า child เรียก `e.stopPropagation()` event จะไม่ bubble ขึ้นมาถึง parent ที่ผูก delegation ไว้