Tác giả NIIT - ICT HANOI
Ngày đăng 13/ 10/ 2020
Bình luận 0 Bình luận
Trong hướng dẫn tự học JavaScript này, bạn sẽ tìm hiểu về
lan truyền sự kiện trong cây DOM trong JavaScript.
Lan truyền sự kiện (Event Propagation) là một cơ chế xác định cách các sự kiện lan truyền hoặc di chuyển qua cây DOM để đến mục tiêu của nó và điều gì xảy ra với nó sau đó.
Giả sử bạn đã chỉ định một trình xử lý sự kiện nhấp chuột trên một siêu kết nối (tức là một phần tử) được lồng bên trong một đoạn văn (tức là phần tử p).
<p onclick="suKienA()" id="myPara">
<a href="#">Click vào đây</a>
</p>
Bây giờ nếu bạn nhấp vào liên kết đó, Event Handler sẽ được thực thi.
Tuy nhiên, nếu bạn chỉ định trình xử lý sự kiện nhấp chuột cho thẻ <p> có chứa thẻ <a>, thì....
Ngay cả trong trường hợp này, việc click vào thẻ <a> vẫn sẽ kích hoạt suKienA().
Đó là bởi vì các sự kiện không chỉ ảnh hưởng đến phần tử mục tiêu đã tạo ra sự kiện — chúng di chuyển lên và xuống qua cây DOM để đạt được mục tiêu của mình.
Đây được gọi là sự lan truyền sự kiện
Trong các trình duyệt hiện đại, sự lan truyền sự kiện diễn ra theo hai giai đoạn:
Trước khi chúng ta tiếp tục, hãy xem hình minh họa sau:
Hình ảnh trên minh họa một hành vi lan truyền sự kiện trong cây DOM.
Lan truyền sự kiện xảy ra khi một sự kiện được kích hoạt trên một phần tử có một hoặc nhiều phần tử cha.
Bởi vì, các phần tử HTML cha con được xếp chồng (lồng vào nhau). Do đó, nếu bạn click vào phần tử bên trong cùng thì cũng tương đương click vào cả cụm cha con đó.
Và nếu chúng đang lắng nghe cùng một loại sự kiện (ví dụ click) thì, trình xử lý sự kiện (Event Listeners) tương ứng sẽ được kích hoạt.
> Đọc thêm: Event Listener trong JavaScript
Khái niệm về sự lan truyền sự kiện được đưa ra để giải quyết các tình huống trong đó nhiều phần tử trong phân cấp DOM có mối quan hệ CHA - CON có các trình xử lý sự kiện cho cùng một sự kiện, chẳng hạn như một cú click chuột.
Bây giờ, câu hỏi là sự kiện click chuột của phần tử nào sẽ được xử lý đầu tiên khi người dùng click vào phần tử bên trong — sự kiện click chuột của phần tử bên ngoài hoặc phần tử bên trong.
Trong các phần tiếp theo của bài học này, chúng ta sẽ thảo luận chi tiết hơn về từng giai đoạn của sự lan truyền sự kiện và tìm ra câu trả lời cho câu hỏi này.
Lưu ý: Về hình thức có 3 giai đoạn, capture, target và giai đoạn bubbling. Tuy nhiên, giai đoạn thứ hai, tức là giai đoạn target (xảy ra khi sự kiện đến phần tử đích đã tạo ra sự kiện) không được xử lý riêng biệt trong các trình duyệt hiện đại, các trình xử lý đã đăng ký cho cả giai đoạn capture và bubbling được thực thi trong giai đoạn này
Trong giai đoạn capturing, các sự kiện truyền từ Window xuống qua cây DOM đến node đích.
Ví dụ: Nếu người dùng click vào thẻ <a> bên trong cùng, sự kiện click chuột đó sẽ chuyển qua phần tử <html>, phần tử <body> và phần tử <p> chứa phần tử <a>.
Ngoài ra, nếu bất kỳ phần tử nào bao quanh (tức là cha, mẹ, ông, bà, v.v.) phần tử đích và bản thân mục tiêu có Event Listener được đăng ký đặc biệt cho loại sự kiện đó, thì những Event Listener đó cũng sẽ được thực thi trong giai đoạn này.
Hãy xem ví dụ sau:
HTML:
<div id="wrap">Thẻ DIV
<p class="hint">Thẻ P
<a href="#">Thẻ A</a>
</p>
</div>
JS:
// Tạo hàm thông báo tên thẻ HTML
function xemTheHTML() {
alert("Bắt được thẻ: "+ this.tagName);
}
// Nhắm đến các thẻ
var mucTieus = document.querySelectorAll("div, p, a");
// Lặp qua các phần tử và thêm Event Listener
for (let phanTu of mucTieu) {
phanTu.addEventListener("click", xemTheHTML, true);
}
CSS một chút cho đẹp mắt hơn nhé:
div, p, a{
padding: 15px 30px;
display: block;
border: 2px solid #003999;
background: #fff;
}
Đây là một minh chứng đơn giản mà chúng ta đã tạo bằng cách sử dụng ví dụ trên để cho bạn thấy cách hoạt động của tính năng Event capturing.
Hãy chạy ví dụ trên ở trên trình duyệt và click vào bất kỳ phần tử nào để quan sát cửa sổ thứ tự cửa sổ cảnh báo được bật lên như thế nào.
Tính năng Event Capturing không được hỗ trợ trong tất cả các trình duyệt và hiếm khi được sử dụng.
Ví dụ: Từ trước phiên bản IE 9.0, nó không hỗ trợ Event Capturing
Ngoài ra, việc thu thập sự kiện chỉ hoạt động với các trình xử lý sự kiện được đăng ký với phương thức addEventListener() khi đối số thứ ba được đặt thành true.
Phương pháp chỉ định trình xử lý sự kiện truyền thống, như sử dụng onclick, onmouseover, v.v. sẽ không hoạt động ở đây.
Đọc lại bài Event Listeners trong JavaScript để tìm hiểu thêm về Event Listener.
Trong giai đoạn bubbling, quá trình sự kiện kích hoạt ngược lại. Nếu capturing thực hiện từ ngoài vào trong thì bubbling thực hiện từ trong ra ngoài.
Ví dụ: Nếu người dùng click vào thẻ <a>, sự kiện click chuột đó sẽ chuyển qua phần tử <p> chứa thẻ <a> đó rồi đến phần tử <body>, phần tử <html> và node document.
Ngoài ra, nếu bất kỳ phần tử nào bao quanh phần tử đích và bản thân phần tử đích có các Event Listener được chỉ định cho loại sự kiện đó, thì các trình xử lý đó sẽ được thực thi trong giai đoạn này.
Trong các trình duyệt hiện đại, theo mặc định, tất cả các Event Listener đều được đăng ký ở giai đoạn Bubbling.
Ta sử dụng lại ví dụ trên nhưng có thay đổi một chút ở phương thức addEventListener().
JS:
// Tạo hàm thông báo tên thẻ HTML
function xemTheHTML() {
alert("Bắt được thẻ: "+ this.tagName);
}
// Nhắm đến các thẻ
var mucTieus = document.querySelectorAll("div, p, a");
// Lặp qua các phần tử và thêm Event Listener
for (let phanTu of mucTieu) {
phanTu.addEventListener("click", xemTheHTML, false);
}
Lưu ý: Ở đây mình mình sử dụng tham số thứ ba của phương thức addEventListener() là false (Mặc định - Không cần chỉ định cũng hiểu là false)
Bạn thử chạy trên trình duyệt xem thứ tự các Event Listener hoạt động thế nào nhé :D
Hãy xem một ví dụ khác mà sử dụng onclick thay vì addEventListener():
<html>
<head>
</head>
<body>
<div onclick="alert('Thẻ: ' + this.tagName)">DIV
<p onclick="alert('Thẻ: ' + this.tagName)">P
<a href="#" onclick="alert('Thẻ: ' + this.tagName)">A</a>
</p>
</div>
</body>
</html>
<html>
Như vậy chúng ta có 2 cách để tạo ra đăng ký sự kiện trong giai đoạn bubbling.
Event Bubbling được hỗ trợ trong tất cả các trình duyệt và nó hoạt động cho tất cả các trình xử lý, bất kể chúng được đăng ký như thế nào.
Phần tử mục tiêu là một node nào đó đã tạo ra sự kiện.
Ví dụ: Nếu người dùng click vào thẻ <a>, phần tử mục tiêu là thẻ <a>
Phần tử mục tiêu có thể truy cập được dưới dạng event.target, nó không thay đổi qua các giai đoạn lan truyền sự kiện.
Ngoài ra, từ khóa this đại diện cho phần tử hiện tại (tức là phần tử có một trình xử lý hiện đang chạy được đính kèm với nó).
Hãy xem một ví dụ (vẫn sử dụng HTML trên)
JS:
// Chọn đến phần tử DIV
var div = document.getElementById("wrap");
// Thêm sự kiện onclick
div.onclick = function(event) {
event.target.style.backgroundColor = "red";
// Thay đổi màu trước khi hiện thông báo
setTimeout(() => {
alert("Mục tiêu = " + event.target.tagName + ", this = " + this.tagName);
event.target.style.backgroundColor = ''
}, 0);
}
Ký hiệu mũi tên (=>) mà chúng ta đã sử dụng trong ví dụ trên là một biểu thức arrow function.
Nó có cú pháp ngắn hơn so với một biểu thức hàm và nó sẽ làm cho từ khóa this hoạt động đúng.
Bạn có thể đọc thêm tính năng của ES6 để tìm hiểu thêm về arrow function.
Bạn cũng có thể ngừng lan truyền sự kiện ở giữa nếu bạn muốn ngăn trình xử lý sự kiện của phần tử cha mẹ được thông báo về sự kiện.
Ví dụ: Giả sử bạn có các phần tử lồng nhau và mỗi phần tử có trình xử lý sự kiện onclick hiển thị hộp thoại cảnh báo. Thông thường, khi bạn nhấp vào phần tử bên trong, tất cả các trình xử lý sẽ được thực thi cùng một lúc.
JS:
// Tạo hàm thông báo tên thẻ
function thongBao() {
alert("Bạn đã click vào: "+ this.tagName);
}
// Thu thập các thẻ
var phanTus = document.querySelectorAll("div, p, a");
// Lặp và thêm Event Listener
for(let pt of phanTus) {
pt.addEventListener("click", thongBao);
}
Nếu bạn click vào bất kỳ phần tử con nào, Event Listeners trên các phần tử cha mẹ cũng được thực thi và bạn có thể thấy nhiều cảnh báo.
Để ngăn chặn tình trạng này, bạn có thể ngăn sự kiện tạo ra cây DOM bằng phương thức event.stopPropagation().
Trong ví dụ sau, trình nghe sự kiện click vào các phần tử cha mẹ sẽ không thực thi nếu bạn bấm vào các phần tử con.
JS:
function thongBao(event) {
alert("Bạn đã click vào: "+ this.tagName);
// Ngừng lan truyền sự kiện
event.stopPropagation();
}
// Thu thập các thẻ
var phanTus = document.querySelectorAll("div, p, a");
// Lặp và thêm Event Listener
for(let pt of phanTus) {
pt.addEventListener("click", thongBao);
}
Bây giờ nếu bạn click vào bất kỳ phần tử con nào, sẽ chỉ một cảnh báo xuất hiện.
Ngoài ra, việc một phần tử có thể có nhiều Event Listener cùng loại.
Và nếu bạn chỉ muốn nó thực thi một sự kiện thì có thể ngăn chặn nó thực thi những cái khác bằng đặt phương thức này vào trong Event Listener bạn muốn.
Trong ví dụ sau, chúng ta đã đính kèm nhiều Listeners vào thẻ <a>, nhưng chỉ một Listener cho link sẽ thực thi khi bạn nhấp vào liên kết và bạn sẽ chỉ thấy một cảnh báo.
HTML:
<div onclick="alert('Bạn đã click vào: ' + this.tagName)">DIV
<p onclick="alert('Bạn đã click vào: ' + this.tagName)">P
<a href="#" id="link">A</a>
</p>
</div>
JS:
function xinChao() {
alert("Xin chào!");
event.stopImmediatePropagation();
}
function chaoMung() {
alert("Chào mừng bạn đến với JS!");
}
// Gắn nhiều Even Listener vào link
var link = document.getElementById("link");
link.addEventListener("click", xinChao);
link.addEventListener("click", chaoMung);
Mặc dù có 2 Event Listeners được thêm vào liên kết.
Nhưng khi click, phương thức xinChao() được thực thi thì phương thức stopImmediatePropagation() có trong phương thức xinChao() sẽ dừng lan truyền sự kiện.
Do đó, bạn chỉ thấy một cảnh báo mà thôi.
Lưu ý: Nếu một số Even Listener được gắn vào cùng một phần tử cho cùng một loại sự kiện, chúng được thực thi theo thứ tự đã được thêm vào. Tuy nhiên, nếu bất kỳ Listener nào gọi phương thức event.stopIm InstantPropagation(), thì các Listener còn lại sẽ không được thực thi.
Một số sự kiện có một hành động mặc định được liên kết với chúng.
Ví dụ: Nếu bạn click vào trình duyệt liên kết sẽ đưa bạn đến liên kết mục tiêu, khi bạn nhấp vào nút gửi biểu mẫu, trình duyệt gửi biểu mẫu, v.v.
Bạn có thể ngăn các hành động mặc định đó bằng phương thức preventDefault() của đối tượng Event.
Tuy nhiên, việc ngăn chặn các hành động mặc định không dừng sự lan truyền sự kiện. Sự kiện tiếp tục truyền đến cây DOM như bình thường.
Đây là một ví dụ:
HTML:
<form action="/examples/html/action.php" method="post" id="users">
<label>Họ và tên:</label>
<input type="text" name="ho-va-ten" id="hoVaTen">
<input type="submit" value="Gửi" id="nutGuiBieuMau">
</form>
/examples/html/action.php là file PHP mà chương trình sẽ được điều hướng đến đó nếu người dùng click vào nút Gửi.
Tuy nhiên, bạn đừng quan tâm nó bây giờ.
Chúng ta ngăn chặn hành vi điều hướng mặc định này bằng JavaScript:
// Nhắm mục tiêu đến nút Gửi
var nutGuiBieuMau = document.getElementById("nutGuiBieuMau");
nutGuiBieuMau.addEventListener("click", function(suKien) {
// Ngăn chặn gửi biểu mẫu
suKien.preventDefault();
// Lấy tên mà người dùng đã nhập
var hoVaTen = document.getElementById("hoVaTen").value;
// Thông báo cho người dùng biết lý do
alert("Xin lỗi, " + hoVaTen + ". The preventDefault() ngăn bạn gửi biểu mẫu!");
});
Ở đoạn mã JavaScript này đơn giản là bạn tạo chương trình để thêm vào nút Gửi một Event Listener là:
Như vậy, qua bài viết này mình đã giới thiệu với bạn về lan truyền sự kiện trong JavaScript (Event Propagation), các giai đoạn bubbling và capturing cũng như cách dừng lan truyền sự kiện và ngăn chặn hành vi mặc định.
Chúc bạn HỌC JAVASCRIPT tốt hơn!