- Hiểu 3 phase của event propagation (capturing, target, bubbling), cách addEventListener với useCapture hoạt động, và khi nào dùng stopPropagation vs stopImmediatePropagation vs preventDefault.
- Kèm code pattern event delegation và các pitfall phổ biến.
TL;DR
Mỗi event trong DOM đi qua 3 phase: capturing (root → target), target, và bubbling (target → root). Mặc định handler chạy ở bubbling. Hiểu đúng flow này giúp bạn dùng được event delegation — một pattern cho phép chỉ bind 1 listener cho cả trăm child element. stopPropagation(), stopImmediatePropagation(), và preventDefault() là 3 method khác nhau cho 3 mục đích khác nhau — dùng sai sẽ tạo ra "dead zone" phá analytics và modal.
Event propagation là gì?
Khi bạn click vào một <button> nằm trong <div> trong <form>, browser không chỉ fire event ở mỗi cái button. Event đi qua toàn bộ chuỗi ancestor theo thứ tự quy định bởi DOM Events spec. Đó chính là event propagation.
Spec định nghĩa 3 phase rõ ràng, tương ứng với giá trị event.eventPhase:
| Phase | eventPhase | Hướng đi |
|---|---|---|
| Capturing | 1 | document → target |
| Target | 2 | tại target |
| Bubbling | 3 | target → document |
Hầu hết event đều bubble. Vài ngoại lệ đáng nhớ: focus, blur, mouseenter, mouseleave không bubble. Thay thế khi cần delegate: focusin / focusout.
Tại sao developer phải quan tâm?
Vì cùng một click, bạn có thể thấy 3 handler khác nhau fire trên 3 element khác nhau — nếu không hiểu thứ tự, bug sẽ xuất hiện kiểu "click vào video thì modal tự đóng" hoặc "click vào menu item thì dropdown cha cũng đóng luôn". Ngoài ra, bubbling mở ra pattern event delegation — lợi thế performance rất lớn khi UI có nhiều child động.
Technical facts cần nhớ
Cú pháp đầy đủ của addEventListener:
element.addEventListener(type, handler, useCapture);
// hoặc
element.addEventListener(type, handler, { capture: true, once: false, passive: false });- Param thứ 3 =
true→ handler fire ở capturing phase. - Param thứ 3 =
false(default) → handler fire ở bubbling phase. - Handler gắn qua HTML attribute (
onclick="...") hoặcelement.onclick =chỉ thấy được phase 2 và 3. Capturing hoàn toàn vô hình với cú pháp này. event.target— element sâu nhất đã kích hoạt event, không đổi trong suốt quá trình propagate.event.currentTarget(=thistrong handler) — element đang chạy handler hiện tại, thay đổi theo từng cấp ancestor.removeEventListenerphải truyền đúng cùng giá trịcaptuređã dùng khiaddEventListener, nếu không sẽ không gỡ được.
stopPropagation vs stopImmediatePropagation vs preventDefault
Ba method này hay bị nhầm lẫn, nhưng chúng làm 3 việc khác nhau:
| Method | Chặn travel capture/bubble | Chặn các handler khác trên cùng element | Chặn default action của browser |
|---|---|---|---|
stopPropagation() | ✅ | ❌ | ❌ |
stopImmediatePropagation() | ✅ | ✅ | ❌ |
preventDefault() | ❌ | ❌ | ✅ |
Một số case cụ thể để phân biệt:
- Click một
<a href="/foo">rồi gọipreventDefault()→ browser không điều hướng, nhưng click vẫn bubble lên parent. - Cũng click
<a>đó nhưng gọistopPropagation()→ vẫn điều hướng sang/foo, nhưng parent handler không được gọi. stopPropagation()gọi ở capturing phase cũng đồng thời chặn luôn bubbling phase — event "chết" tại đó.
Event delegation — use case kinh điển
Giả sử bạn có một grid 16 tile, mỗi tile đổi màu random khi click. Cách naive:
document.querySelectorAll('.tile').forEach(tile => {
tile.addEventListener('click', () => {
tile.style.background = randomColor();
});
});Với 16 tile là OK. Với 1000 row trong table hoặc list render động thì toi. Cách delegation:
const grid = document.querySelector('.grid');
grid.addEventListener('click', (e) => {
const tile = e.target.closest('.tile');
if (!tile) return;
tile.style.background = randomColor();
});Chỉ 1 listener, hoạt động cho cả tile được thêm sau khi render, tiết kiệm memory và đơn giản hoá cleanup. e.target.closest('.tile') là pattern chuẩn để tìm đúng child cần xử lý bất kể click trúng layer HTML nào bên trong tile.
Những tình huống khác dùng delegation/propagation:
- Click outside to close: listen
clicktrêndocument, check!dropdown.contains(e.target)→ đóng dropdown. - Global keyboard shortcut: listen trên
windowở phase capturing để bắt phím trước khi bất kỳ child nào xử lý. - Analytics tracking: một listener duy nhất trên
documentđể log mọi click user tạo ra.
Pitfalls — cẩn thận kẻo tự bắn vào chân
- Đừng stopPropagation lung tung. Mỗi lần bạn gọi, bạn tạo một "dead zone" — nơi các global listener (analytics, modal-close-on-outside, shortcut manager) không còn thấy event. Phần lớn case có thể giải quyết bằng logic bên trong handler (check
e.target) thay vì chặn event. - React synthetic events khác native events. Trong React 17+,
e.stopPropagation()chỉ dừng propagation trong cây virtual của React. Native listener trêndocumentvẫn nhận được. Nếu cần chặn cả hai, dùnge.nativeEvent.stopImmediatePropagation(). - Capture không
preventDefaultđược child link. GọipreventDefault()ở phase capturing vẫn chạy, nhưng để huỷ navigation của<a>bạn phải gọi trên đúng element hoặc trong bubbling handler. - Focus không bubble. Dùng
focusin/focusouthoặc đăng ký listener ở capturing phase thay vì cố delegatefocus.
Khi nào nên dùng capturing?
95% use case đời thực chỉ cần bubbling (default). Capturing đáng dùng khi parent bắt buộc phải intercept trước child: focus trap trong modal, global hotkey interceptor, hoặc analytics/security layer cần log event ngay cả khi một handler con gọi stopPropagation trong bubbling.
Tóm lại: hiểu 3 phase, thuộc 3 method control, mặc định delegate thay vì bind loose listener, và chỉ stopPropagation khi bạn biết chính xác mình đang chặn cái gì.
Nguồn: javascript.info — Bubbling and capturing, MDN — Event.stopPropagation(), MDN Learn — Event bubbling.

