Cafedev chia sẻ cho ace về Cơ chế Sủi bọt và nắm bắt(Bubbling and capturing) trong Javascript và cách hoạt động của nó với event trong trình duyệt.
Hãy bắt đầu với một ví dụ.
Trình xử lý này được gán cho <div>
, nhưng cũng chạy nếu bạn nhấp vào bất kỳ thẻ lồng nhau nào như <em>
hoặc <code>
:
<div onclick="alert('The handler!')">
<em>If you click on <code>EM</code>, the handler on <code>DIV</code> runs.</em>
</div>
Nó không phải là một chút lạ? Tại sao trình xử lý <div>
chạy nếu nhấp chuột thực sự được bật <em>
?
Nội dung chính
1. Sủi bọt(Bubbling)
Nguyên tắc sủi bọt rất đơn giản.
Khi một sự kiện xảy ra trên một phần tử, trước tiên nó chạy các trình xử lý trên nó, sau đó là cha mẹ của nó, sau đó tiếp tục lên các tổ tiên khác.
Giả sử chúng ta có 3 phần tử lồng nhau FORM > DIV > P
với một trình xử lý trên mỗi phần tử :
<style>
body * {
margin: 10px;
border: 1px solid blue;
}
</style>
<form onclick="alert('form')">FORM
<div onclick="alert('div')">DIV
<p onclick="alert('p')">P</p>
</div>
</form>
Một cú nhấp chuột vào <p>
lần chạy đầu tiên bên trong onclick:
- Trên đó
<p>
. - Sau đó, ở bên ngoài
<div>
. - Sau đó, ở bên ngoài
<form>
. - Và cứ như vậy trở lên cho đến đối tượng document.
Vì vậy, nếu chúng ta nhấp vào <p>
, thì chúng ta sẽ thấy 3 cảnh báo: p→ div→ form.
Quá trình này được gọi là “sủi bọt”, bởi vì các sự kiện “sủi bọt” từ phần tử bên trong lên qua các bậc cha mẹ giống như bong bóng trong nước.
Hầu hết tất cả các sự kiện Sủi bọt.
Từ khóa trong cụm từ này là “hầu hết”.
Ví dụ, một sự kiện focus không Sủi bọt. Có những ví dụ khác nữa, chúng ta sẽ gặp chúng. Nhưng đó vẫn là một ngoại lệ, thay vì một quy luật, hầu hết các sự kiện đều có Sủi bọt.
2. event.target
Một trình xử lý trên phần tử mẹ luôn có thể nhận được thông tin chi tiết về nơi nó thực sự xảy ra.
Phần tử lồng nhau sâu nhất đã gây ra sự kiện được gọi là phần tử đích , có thể truy cập bằng event.target.
Lưu ý sự khác biệt từ this(= event.currentTarget):
- event.target – là phần tử “đích” đã bắt đầu sự kiện, nó không thay đổi trong quá trình tạo bọt.
- this – là phần tử “hiện tại”, phần tử có một trình xử lý hiện đang chạy trên đó.
Ví dụ: nếu chúng ta có một trình xử lý duy nhất form.onclick, thì nó có thể “bắt” tất cả các nhấp chuột bên trong biểu mẫu. Bất kể lần nhấp xảy ra ở đâu, nó sẽ nổi lên < form > và chạy trình xử lý.
Trong form.onclick trình xử lý:
- this(= event.currentTarget) là phần tử
<form>
, vì trình xử lý chạy trên nó. - event.target là phần tử thực tế bên trong biểu mẫu đã được nhấp vào.
form.onclick = function(event) {
event.target.style.backgroundColor = 'yellow';
// chrome needs some time to paint yellow
setTimeout(() => {
alert("target = " + event.target.tagName + ", this=" + this.tagName);
event.target.style.backgroundColor = ''
}, 0);
};
event.target có thể bằng nhau với this – nó xảy ra khi nhấp chuột được thực hiện trực tiếp trên phần tử <form>
.
3. Ngừng sủi bọt
Một sự kiện sôi sục đi từ phần tử mục tiêu thẳng lên. Thông thường, nó đi lên cho đến <html>
, rồi đến document phản đối, và một số sự kiện thậm chí đạt đến window, gọi tất cả các trình xử lý trên đường dẫn.
Nhưng bất kỳ người xử lý nào cũng có thể quyết định rằng sự kiện đã được xử lý hoàn toàn và ngừng sủi bọt.
Phương thức cho nó là event.stopPropagation().
Ví dụ: ở đây body.onclick không hoạt động nếu bạn nhấp vào<button>
:
<body onclick="alert(`the bubbling doesn't reach here`)">
<button onclick="event.stopPropagation()">Click me</button>
</body>
event.stopImmediatePropagation()
Nếu một phần tử có nhiều trình xử lý sự kiện trên một sự kiện, thì ngay cả khi một trong số chúng dừng quá trình sôi, những phần tử còn lại vẫn thực thi.
Nói cách khác, event.stopPropagation() dừng di chuyển lên trên, nhưng trên phần tử hiện tại, tất cả các trình xử lý khác sẽ chạy.
Để ngăn hiện tượng nổi bọt và ngăn các trình xử lý trên phần tử hiện tại chạy, có một phương pháp event.stopImmediatePropagation(). Sau khi nó không có trình xử lý nào khác thực thi.
Đừng ngừng sủi bọt khi không có nhu cầu!
Sủi bọt là thuận tiện. Đừng dừng nó lại khi không có nhu cầu thực sự: rõ ràng và có kiến trúc tốt.
Đôi khi event.stopPropagation() tạo ra những cạm bẫy tiềm ẩn mà sau này có thể trở thành vấn đề.
Ví dụ:
- Chúng tôi tạo một menu lồng nhau. Mỗi menu con xử lý các lần nhấp vào các phần tử và lệnh gọi của nó stopPropagation để menu bên ngoài không kích hoạt.
- Sau đó, chúng tôi quyết định bắt các lần nhấp trên toàn bộ cửa sổ, để theo dõi hành vi của người dùng (nơi mọi người nhấp vào). Một số hệ thống phân tích làm được điều đó. Thông thường code sử dụng document.addEventListener(‘click’…)để bắt tất cả các nhấp chuột.
- Phân tích của chúng ta sẽ không hoạt động trên khu vực mà các nhấp chuột bị dừng lại stopPropagation. Đáng buồn thay, chúng ta đã có một “vùng chết”.
Thường không cần thực sự ngăn chặn sự sủi bọt. Một nhiệm vụ dường như yêu cầu nhưng có thể được giải quyết bằng cách khác. Một trong số đó là sử dụng các sự kiện tùy chỉnh, chúng ta sẽ trình bày sau. Ngoài ra, chúng ta có thể ghi dữ liệu của mình vào đối tượng event trong một trình xử lý và đọc nó trong một trình xử lý khác, vì vậy chúng ta có thể chuyển cho trình xử lý trên cha mẹ thông tin về quá trình xử lý bên dưới.
4. Bắt giữ(capturing)
Có một giai đoạn xử lý sự kiện khác được gọi là “bắt giữ”. Nó hiếm khi được sử dụng trong code thực, nhưng đôi khi có thể hữu ích.
Sự kiện DOM chuẩn mô tả 3 giai đoạn lan truyền sự kiện:
- Giai đoạn nắm bắt – sự kiện đi xuống phần tử.
- Giai đoạn mục tiêu – sự kiện đạt đến phần tử mục tiêu.
- Giai đoạn sủi bọt – sự kiện bong bóng lên từ phần tử.
Đây là hình ảnh của một cú nhấp chuột vào <td>
bên trong bảng, được lấy từ thông số kỹ thuật:
Đó là: đối với một lần nhấp vào <td>
sự kiện trước tiên đi qua chuỗi tổ tiên xuống phần tử (giai đoạn bắt giữ), sau đó nó đến mục tiêu và kích hoạt ở đó (giai đoạn mục tiêu), sau đó nó đi lên (giai đoạn sôi sục), gọi trình xử lý trên theo cách của nó.
Trước đây chúng ta chỉ nói đến việc sủi bọt, vì giai đoạn bắt giữ hiếm khi được sử dụng. Bình thường nó là vô hình với chúng ta.
Các trình xử lý được thêm bằng thuộc tínhon<event>
hoặc sử dụng thuộc tính HTML hoặc sử dụng hai đối số addEventListener(event, handler) không biết gì về việc nắm bắt, chúng chỉ chạy ở giai đoạn 2 và 3.
Để nắm bắt một sự kiện trong giai đoạn bắt, chúng ta cần đặt capture tùy chọn xử lý thành true:
elem.addEventListener(..., {capture: true})
// or, just "true" is an alias to {capture: true}
elem.addEventListener(..., true)
Có hai giá trị có thể có của capture tùy chọn:
- Nếu là false(mặc định), thì trình xử lý được đặt ở giai đoạn sủi bọt.
- Nếu có true, thì trình xử lý được đặt ở giai đoạn bắt.
Lưu ý rằng mặc dù chính thức có 3 giai đoạn, nhưng giai đoạn thứ 2 (“giai đoạn mục tiêu”: sự kiện đạt đến phần tử) không được xử lý riêng biệt: các trình xử lý trên cả giai đoạn bắt giữ và tạo bọt sẽ kích hoạt ở giai đoạn đó.
Hãy xem cả hai hoạt động bắt giữ và sôi sục:
<style>
body * {
margin: 10px;
border: 1px solid blue;
}
</style>
<form>FORM
<div>DIV
<p>P</p>
</div>
</form>
<script>
for(let elem of document.querySelectorAll('*')) {
elem.addEventListener("click", e => alert(`Capturing: ${elem.tagName}`), true);
elem.addEventListener("click", e => alert(`Bubbling: ${elem.tagName}`));
}
</script>
Bộ code các trình xử lý nhấp chuột trên mọi phần tử trong tài liệu để xem những phần tử nào đang hoạt động.
Nếu bạn nhấp vào <p>
, thì trình tự là:
HTML
→BODY
→FORM
→DIV
(giai đoạn nắm bắt, người nghe đầu tiên):- P (giai đoạn mục tiêu, kích hoạt hai lần, vì chúng ta đã thiết lập hai người nghe: nắm bắt và sôi sục)
DIV
→FORM
→BODY
→HTML
(giai đoạn sủi bọt, người nghe thứ hai).
Có một thuộc tính event.eventPhase cho chúng ta biết số giai đoạn mà sự kiện bị bắt. Nhưng nó hiếm khi được sử dụng, vì chúng ta thường biết nó trong trình xử lý.
Để loại bỏ trình xử lý, cần removeEventListener cùng một giai đoạn
Nếu chúng ta addEventListener(…, true), thì chúng ta nên đề cập đến cùng một giai đoạn removeEventListener(…, true) để loại bỏ chính xác trình xử lý.
Người nghe trên cùng một phần tử và cùng một giai đoạn chạy theo thứ tự đã đặt của chúng
Nếu chúng ta có nhiều trình xử lý sự kiện trên cùng một pha, được gán cho cùng một phần tử với addEventListener, chúng sẽ chạy theo thứ tự giống như khi chúng được tạo:
elem.addEventListener("click", e => alert(1)); // guaranteed to trigger first
elem.addEventListener("click", e => alert(2));
5. Tóm lược
Khi một sự kiện xảy ra – phần tử lồng nhau nhất nơi nó xảy ra sẽ được gắn nhãn là “phần tử đích” ( event.target).
- Sau đó, sự kiện di chuyển xuống từ gốc tài liệu đến event.target, gọi các trình xử lý được chỉ định addEventListener(…, true)trên đường ( truelà cách viết tắt của {capture: true}).
- Sau đó, các trình xử lý được gọi trên chính phần tử đích.
- Sau đó, sự kiện bong bóng từ event.target lên đến gốc, gọi các trình xử lý được chỉ định bằng cách sử dụng on< event >và addEventListener không có đối số thứ 3 hoặc với đối số thứ 3 false/{capture:false}.
Mỗi trình xử lý có thể truy cập các thuộc tính đối tượng event:
- event.target – yếu tố sâu xa nhất bắt nguồn sự kiện.
- event.currentTarget(= this) – phần tử hiện tại xử lý sự kiện (phần tử có trình xử lý trên đó)
- event.eventPhase – pha hiện tại (bắt = 1, mục tiêu = 2, sủi bọt = 3).
Bất kỳ trình xử lý sự kiện nào cũng có thể dừng sự kiện bằng cách gọi event.stopPropagation(), nhưng điều đó không được khuyến khích, vì chúng ta không thể thực sự chắc chắn rằng mình sẽ không cần nó ở trên, có thể cho những thứ hoàn toàn khác.
Giai đoạn bắt giữ rất hiếm khi được sử dụng, thông thường chúng ta xử lý các sự kiện bằng cách tạo bọt. Và có một logic đằng sau đó.
Trong thực tế, khi một vụ tai nạn xảy ra, chính quyền địa phương sẽ phản ứng đầu tiên. Họ biết rõ nhất khu vực nơi nó đã xảy ra. Sau đó mới đến chính quyền cấp cao hơn nếu cần.
Đối với các trình xử lý sự kiện cũng vậy. Code đặt trình xử lý trên một phần tử cụ thể biết chi tiết tối đa về phần tử và chức năng của phần tử đó. Một người xử lý về một thứ cụ thể <td>
có thể phù hợp với điều đó chính xác <td>
, nó biết mọi thứ về nó, vì vậy nó nên có cơ hội trước. Sau đó, cha mẹ trực tiếp của nó cũng biết về ngữ cảnh, nhưng ít hơn một chút, và cứ tiếp tục như vậy cho đến phần tử trên cùng xử lý các khái niệm chung và chạy cuối cùng.
Sủi bọt và chụp ảnh đặt nền tảng cho “ủy quyền sự kiện” – một mẫu xử lý sự kiện cực kỳ hiệu quả mà chúng ta sẽ nghiên cứu trong chương tiếp theo.
Nguồn và tài liệu tiếng anh tham khảo:
Full series tự học Javascript từ cơ bản tới nâng cao tại đây nha.
Nếu bạn thấy hay và hữu ích, bạn có thể tham gia các kênh sau của cafedev để nhận được nhiều hơn nữa:
Chào thân ái và quyết thắng!