Event Bubbling trong DOM: Đừng chặn nó, hãy lợi dụng nó! (Event Delegation)
Hãy tưởng tượng bạn đang code một trang web. Bạn có một thẻ <div> lớn đóng vai trò là một cái Card. Nhấn vào Card sẽ chuyển hướng sang trang chi tiết. Bên trong cái Card đó, bạn làm thêm một nút <button> nhỏ để "Thêm vào giỏ hàng".
Biến cố xảy ra: Khi bạn click vào nút "Thêm vào giỏ hàng", vật phẩm thì được thêm đấy, nhưng trang web của bạn lại... tự động chuyển luôn sang trang chi tiết (do cái Card bị click ké). Ủa, mình click vào cái nút cơ mà, sao cái Card lại nhận được sự kiện?
Chào mừng bạn đến với thế giới của Event Bubbling (Sự kiện nổi bọt).
1. Bản chất của Event Bubbling
Trong HTML DOM, các phần tử không đứng độc lập mà nằm lồng vào nhau theo kiến trúc cây (Tree). Khi một sự kiện (như click, hover) xảy ra trên một phần tử, sự kiện đó không chỉ nằm chết ở phần tử đó. Nó sẽ hoạt động giống như một bong bóng khí dưới đáy hồ: Nổi dần lên mặt nước.
Cụ thể, sự kiện sẽ kích hoạt tại phần tử con (Child), sau đó lan truyền ngược lên phần tử cha bọc nó (Parent), tiếp tục lên ông nội (Grandparent), và cứ thế lan mãi lên tận gốc của tài liệu (<html> và document).
Ví dụ cấu trúc:
<div id="grandparent" onclick="console.log('Grandparent bị click')">
<div id="parent" onclick="console.log('Parent bị click')">
<button id="child" onclick="console.log('Child bị click')">Click me!</button>
</div>
</div>
Nếu bạn click vào thẻ <button>, tab Console sẽ in ra lần lượt:
Child bị clickParent bị clickGrandparent bị click
2. Cách chặn hiện tượng nổi bọt
Để giải quyết cái bug "Click nút Mua hàng nhưng bị chuyển trang" ở đầu bài, bạn cần đâm vỡ cái bong bóng đó trước khi nó kịp nổi lên đến cái Card.
JavaScript cung cấp một hàm mặc định nằm trong Event Object mang tên stopPropagation() (Dừng lan truyền).
const button = document.getElementById('child');
button.addEventListener('click', function(event) {
// Thực hiện logic thêm vào giỏ hàng
addToCart();
// Đâm vỡ bong bóng, không cho sự kiện lan lên thẻ Div bọc ngoài!
event.stopPropagation();
});
Tuy nhiên, đừng lạm dụng stopPropagation(). Việc chặn sự kiện lan truyền đôi khi sẽ giết chết các thư viện Analytics (như Google Analytics thường lắng nghe cú click ở cấp độ document để theo dõi hành vi người dùng).
3. Tuyệt chiêu tối ưu hiệu năng: Event Delegation
Các "Junior" thường sợ Event Bubbling và tìm cách chặn nó. Nhưng các "Senior" thì coi nó là một mỏ vàng để tối ưu hóa bộ nhớ (Performance) thông qua kỹ thuật Event Delegation (Ủy quyền sự kiện).
Bài toán: Giả sử bạn có một danh sách <ul> chứa 10.000 thẻ <li> (danh sách user chẳng hạn). Bạn muốn khi click vào bất kỳ thẻ <li> nào thì in ra tên của user đó.
Cách làm tồi (Tốn RAM): Bạn dùng vòng lặp for đi qua 10.000 thẻ <li> và gắn cho MỖI thẻ một hàm addEventListener. Hệ thống sẽ phải tạo ra 10.000 cái hàm theo dõi trong bộ nhớ. Trình duyệt của bạn sẽ gào thét!
Cách làm chuẩn Senior (Dùng Bubbling): Lợi dụng việc "bong bóng" từ thẻ <li> kiểu gì cũng sẽ nổi lên thẻ <ul>, ta KHÔNG GẮN sự kiện cho thẻ <li> nào cả. Ta chỉ gắn ĐÚNG MỘT SỰ KIỆN DUY NHẤT vào thẻ <ul>.
// Gắn sự kiện lắng nghe vào phần tử cha (<ul>)
const list = document.getElementById('user-list');
list.addEventListener('click', function(event) {
// event.target chính là phần tử con (<li>) sâu nhất mà user THỰC SỰ click vào
const clickedElement = event.target;
// Kiểm tra xem user có click đúng vào thẻ <li> không
if (clickedElement.tagName === 'LI') {
console.log("Bạn vừa click vào: ", clickedElement.innerText);
}
});
Lợi ích khổng lồ của Event Delegation:
Tiết kiệm RAM tuyệt đối: Chỉ tạo đúng 1 hàm lắng nghe thay vì 10.000 hàm.
Dynamic UI (Dữ liệu động): Nếu bạn dùng JavaScript để chèn thêm một thẻ
<li>mới toanh vào danh sách, thẻ<li>mới này VẪN CÓ THỂ ĐƯỢC CLICK bình thường mà không cần phải gán lại sự kiện cho nó (vì thẻ<ul>cha vẫn luôn đứng đó lắng nghe mọi thứ nổi lên).
4. Lời kết
Event Bubbling là một minh chứng cho thấy: Đôi khi những thứ tưởng chừng như là "Bug" (lỗi) lại là "Feature" (tính năng) tuyệt vời nếu bạn hiểu sâu bản chất kiến trúc của nó. Nắm vững cơ chế Nổi bọt và áp dụng Ủy quyền sự kiện (Delegation) là bước bắt buộc để bạn thiết kế được một UI Framework của riêng mình, hệt như cách React hay Vue xử lý DOM dưới nền!