Bài tập

Giới thiệu về cách sự kiện hoạt động trong JavaScript

Huy Erick

Sự kiện và hành động xảy ra trong hệ thống bạn đang lập trình, là cách hệ thống thông báo cho bạn biết để bạn có thể phản ứng với nó theo nhiều cách khác...

Sự kiện và hành động xảy ra trong hệ thống bạn đang lập trình, là cách hệ thống thông báo cho bạn biết để bạn có thể phản ứng với nó theo nhiều cách khác nhau. Ví dụ, nếu người dùng nhấp vào một nút trên trang web, bạn có thể muốn phản ứng bằng cách hiển thị một thông tin nào đó.

Mỗi sự kiện có sẵn đều có một "event handler", đó là một khối mã (thường là một hàm JavaScript bạn tạo ra) sẽ được chạy khi sự kiện được kích hoạt. Lưu ý rằng có thể gọi nó là "event listeners" - chúng có thể hoán đổi lẫn nhau vì mục đích của chúng giống nhau, mặc dù thực tế, chúng hoạt động cùng nhau.

Lưu ý: Sự kiện web không phải là một phần của ngôn ngữ JavaScript cốt lõi - chúng được định nghĩa là một phần của các API được tích hợp sẵn trong trình duyệt.

Có một số cách để bạn thêm mã xử lý sự kiện vào trang web. Trong phần này, chúng ta sẽ xem xét một số cách và thảo luận về cách chúng ta nên sử dụng cách nào.

1.1. Các thuộc tính xử lý sự kiện của Event Handler

Có rất nhiều thuộc tính tồn tại chứa mã xử lý sự kiện mà bạn có thể gặp phổ biến:

const btn = document.querySelector('button');
btn.onclick = function() {
  const rndCol = 'rgb(' + random(255) + ',' + random(255) + ',' + random(255) + ')';
  document.body.style.backgroundColor = rndCol;
}

Ví dụ trên là một ví dụ, còn rất nhiều thuộc tính khác.

  • btn.onfocusbtn.onblur: Màu sẽ thay đổi khi nút được nhấn và nhả ra; hãy thử nhấn phím tab để tập trung vào nút, sau đó nhấn tab một lần nữa để chuyển sự tập trung ra khỏi nút. Chúng thường được sử dụng để hiển thị thông tin về cách điền vào các trường biểu mẫu khi chúng được tập trung, hoặc hiển thị thông báo lỗi nếu trường biểu mẫu vừa được điền với giá trị không chính xác.

  • btn.ondblclick: Màu chỉ thay đổi khi nút được nhấp đúp.

  • window.onkeypress, window.onkeydown, window.onkeyup: Màu sẽ thay đổi khi một phím được nhấn trên bàn phím. Sự kiện keypress đề cập đến một lần nhấn chung (nhấn và sau đó nhả ra), trong khi keydownkeyup chỉ đề cập đến phần nhấn và phần nhả ra của biểu tượng phím. Lưu ý rằng nó không hoạt động nếu bạn cố gắng đăng ký trình xử lý sự kiện này trên chính nút - bạn phải đăng ký nó trên đối tượng cửa sổ, đại diện cho cả cửa sổ trình duyệt.

  • btn.onmouseoverbtn.onmouseout: Màu sẽ thay đổi khi con trỏ chuột được di chuyển để bắt đầu di chuột qua nút, hoặc khi con trỏ dừng di chuyển qua nút và di chuyển ra khỏi nó.

Có một số sự kiện rất phổ biến và khả dụng cho bất kỳ phần tử nào (ví dụ: xử lý sự kiện onclick có thể đăng ký với gần như bất kỳ phần tử nào), nhưng có một số thuộc tính đặc biệt chỉ hữu ích trong một số tình huống cụ thể (ví dụ: onplay chỉ khả dụng với một số phần tử nhất định như <video>).

1.2. Inline Event Handlers - Đừng sử dụng chúng

Bạn cũng có thể nhìn thấy mẫu mã như sau trong mã của bạn:

<button onclick="bgChange()">Nhấn vào tôi</button>
function bgChange() {
  const rndCol = 'rgb(' + random(255) + ',' + random(255) + ',' + random(255) + ')';
  document.body.style.backgroundColor = rndCol;
}

Hoặc thêm mã JavaScript trực tiếp vào thuộc tính như ví dụ dưới đây:

<button onclick="alert('Xin chào, đây là xử lý sự kiện theo cách cũ của tôi!');">Nhấn vào tôi</button>

Bạn có thể tìm thấy nhiều thuộc tính HTML tương đương khác cho nhiều thuộc tính xử lý sự kiện, nhưng bạn không nên sử dụng chúng - đó là một thực hành không tốt. Nó thực sự khó quản lý mã và không hiệu quả.

Dù bạn đang bắt đầu từ đầu, bạn không nên kết hợp tệp HTML với mã JS của bạn thành một thứ, vì nó khó phân tích cú pháp, có những cái tốt hơn bằng cách giữ tất cả các tệp JS của bạn ở một nơi khác - bạn có thể áp dụng mỗi tệp JS cho nhiều tệp HTML khác nhau.

Ngay cả khi bạn giữ chúng trong một tệp, các bộ xử lý sự kiện trực tuyến cũng không phải là một ý tưởng tốt. Một nút thì OK, nhưng nếu bạn có 100 nút, bạn sẽ phải thêm 100 thuộc tính vào tệp, bây giờ việc bảo trì mã sẽ trở thành ác mộng. Với JS, bạn có thể thêm một hàm xử lý sự kiện cho tất cả các nút trên trang web mà không cần quan tâm đến điều này:

const buttons = document.querySelectorAll('button');
for (let i = 0; i < buttons.length; i++) {
  buttons[i].onclick = bgChange;
}

Hoặc một tùy chọn khác là sử dụng phương thức nội tại forEach có sẵn của các đối tượng NodeList:

buttons.forEach(function(button) {
  button.onclick = bgChange;
});

1.3. addEventListener() và removeEventListener()

Phiên bản mới nhất của chế độ xử lý sự kiện được định nghĩa trong đối tượng DOM cấp độ 2, cung cấp cho trình duyệt một hàm mới là addEventListener(). Hàm này tương tự như thuộc tính xử lý sự kiện, nhưng cú pháp có một số khác biệt:

const btn = document.querySelector('button');
function bgChange() {
  const rndCol = 'rgb(' + random(255) + ',' + random(255) + ',' + random(255) + ')';
  document.body.style.backgroundColor = rndCol;
}
btn.addEventListener('click', bgChange);

Trong hàm addEventListener(), có hai tham số - tên sự kiện mà chúng ta muốn đăng ký và mã mã bao gồm hàm xử lý chúng ta muốn chạy để phản ứng lại sự kiện. Lưu ý rằng bạn có thể đặt toàn bộ mã trong hàm addEventListener() như một hàm vô danh như sau:

btn.addEventListener('click', function() {
  var rndCol = 'rgb(' + random(255) + ',' + random(255) + ',' + random(255) + ')';
  document.body.style.backgroundColor = rndCol;
});

Cơ chế này có một số lợi ích so với hai cơ chế trước. Đầu tiên, nó có một hàm đối tác là removeEventListener() để xóa bộ lắng nghe trước đó đã được thêm. Ví dụ dưới đây sẽ xóa lắng nghe được thêm ở trên:

btn.removeEventListener('click', bgChange);

Điều này không có nghĩa quá quan trọng đối với các chương trình nhỏ, nhưng đối với các chương trình lớn, phức tạp hơn, nó có thể vô cùng hiệu quả để dọn dẹp các bộ xử lý sự kiện không cần thiết. Ngoài ra, ví dụ, điều này cho phép bạn có cùng một nút thực hiện các hành động khác nhau trong các trường hợp khác nhau - tất cả những gì bạn cần làm là thêm hoặc xóa các bộ xử lý sự kiện.

Thứ hai, bạn cũng có thể đăng ký nhiều bộ xử lý cho cùng một trình lắng nghe sự kiện. Hai bộ xử lý dưới đây không thể áp dụng:

myElement.onclick = functionA;
myElement.onclick = functionB;

Dòng thứ hai ghi đè giá trị onclick được đặt bởi dòng đầu tiên. Trong trường hợp đó, bạn cần thay thế nó như sau:

myElement.addEventListener('click', functionA);
myElement.addEventListener('click', functionB);

Khi đó, cả hai hàm đều được thực thi khi các phần tử được nhấp chuột.

Ngoài ra, nó còn nhiều lợi ích khác thông qua các tùy chọn của sự kiện, bạn có thể tìm hiểu thêm tại trang chủ của nó.

1.4. Cơ chế được sử dụng như thế nào?

Trong cả ba cơ chế, bạn không nên sử dụng các thuộc tính xử lý sự kiện HTML, chúng đã lỗi thời và là một thực hành không tốt như đã đề cập ở trên.

Hai cơ chế còn lại có thể thay thế tương đối cho nhau, ít nhất đối với mục đích đơn giản:

  • Thuộc tính xử lý sự kiện có sức mạnh và tùy chọn ít hơn, nhưng nó lại có khả năng tương thích với các trình duyệt (hỗ trợ trình duyệt IE8). Bạn nên bắt đầu với cơ chế này nếu bạn đang học JS.

  • DOM Level 2 Events (addEventListener(), v.v.) có nhiều sức mạnh hơn nhưng cũng phức tạp và không tương thích với nhiều trình duyệt (hỗ trợ IE9). Bạn nên sử dụng khi bạn đã có kinh nghiệm.

Lợi ích chính của cơ chế thứ ba là bạn có thể xóa mã xử lí sự kiện nếu cần, sử dụng removeEventListener() và thêm nhiều lắng nghe với cùng một sự kiện cho phần tử nếu cần. Ví dụ, bạn có thể gọi addEventListener('click', function() {...}) trên một phần tử nhiều lần với nhiều hàm khác nhau trong đối số thứ hai. Điều này không thể thực hiện với thuộc tính xử lý sự kiện vì một số gán trong lần sau sẽ ghi đè lên lần trước đó, ví dụ:

element.onclick = function1;
element.onclick = function2;

Trong trường hợp này, bạn cần:

element.addEventListener('click', function1);
element.addEventListener('click', function2);

Khi đó, cả hai hàm sẽ được thực thi khi phần tử được nhấp chuột.

Ngoài ra còn nhiều lợi ích khác thông qua các tùy chọn của sự kiện, bạn có thể tìm hiểu thêm tại trang chủ.

2.1. Đối tượng Sự kiện

Đôi khi chúng ta truyền một đối tượng vào hàm xử lý sự kiện với tên như event, evt hoặc ngắn gọn là e. Đó được gọi là đối tượng sự kiện và nó được tự động truyền vào hàm xử lý sự kiện để thêm các chức năng và thông tin. Ví dụ:

function bgChange(e) {
  const rndCol = 'rgb(' + random(255) + ',' + random(255) + ',' + random(255) + ')';
  e.target.style.backgroundColor = rndCol;
  console.log(e);
}
btn.addEventListener('click', bgChange);

Bạn có thể xem mã đầy đủ tại đây.

Lưu ý: Bạn có thể sử dụng bất kỳ tên nào bạn muốn cho đối tượng sự kiện - bạn chỉ cần chọn một tên mà sau đó bạn có thể sử dụng để truy cập nó bên trong hàm xử lý sự kiện. event/evt/event là những tên thường được sử dụng bởi các nhà phát triển vì chúng ngắn và dễ nhớ. Luôn tốt khi nhất quán - với chính bạn và với người khác nếu có thể.

2.2. Sự kiện Bubbling và Capture

Chủ đề cuối cùng mà chúng ta muốn nói đến là điều mà bạn có thể không gặp thường xuyên nhưng nó có thể gây ra lỗi nếu bạn không hiểu rõ.

Bubbling và capture là hai cơ chế mô tả những gì xảy ra khi hai hàm xử lý cho cùng một sự kiện được kích hoạt trong một phần tử. Hãy xem ví dụ sau để hiểu rõ hơn:

Một ví dụ đơn giản là hiển thị và ẩn một <div> chứa <video>:

<button>Hiển thị video</button>
<div class="hidden">
  <video>
    <source src="rabbit320.mp4" type="video/mp4">
    <source src="rabbit320.webm" type="video/webm">
    <p>Trình duyệt của bạn không hỗ trợ video HTML5. Dưới đây là <a href="rabbit320.mp4">liên kết tới video</a> thay thế.</p>
  </video>
</div>

Khi bạn nhấp vào nút, video sẽ được hiển thị bằng cách thay đổi thuộc tính class của <div> từ "hidden" thành "showing":

btn.onclick = function() {
  videoBox.setAttribute('class', 'showing');
}

Tiếp theo, có hai sự kiện onclick - một cho <div> và một cho <video>. Ý tưởng là khi khu vực <div> bên ngoài video được nhấp, hộp này sẽ bị ẩn; khi video được nhấp, video sẽ bắt đầu chạy:

videoBox.onclick = function() {
  videoBox.setAttribute('class', 'hidden');
};

video.onclick = function() {
  video.play();
};

Nhưng có một vấn đề - hiện tại, khi bạn nhấp vào video để bắt đầu chạy, nó cũng là <div> bị ẩn vào cùng thời điểm. Điều này xảy ra vì <video> nằm bên trong <div>, vì vậy khi bạn nhấp vào video, thực sự bạn đã chạy cả hai sự kiện.

2.3. Giải thích Bubbling và Capture

Khi một sự kiện được phát ra từ một phần tử, các trình duyệt hiện đại chạy qua hai giai đoạn:

  • Giai đoạn "capture": Trình duyệt kiểm tra xem có tổ tiên bên ngoài gần nhất của html (tức là tổ tiên bên ngoài nhất) có một sự kiện onclick được đăng ký trong giai đoạn capture và chạy nó nếu có. Sau đó, nó chuyển đến phần tử tiếp theo trong <html> và làm tương tự, sau đó tiếp tục cho đến khi nó đến phần tử mà thực sự đã bị nhấp vào.

  • Giai đoạn "bubbling", trái ngược với capture:

    • Trình duyệt sẽ kiểm tra xem có phần tử thực sự bị nhấp vào có onclick được đăng ký trong giai đoạn bubbling và chạy nó nếu có.
    • Sau đó, nó chuyển đến các phần tử cha ngay lập tức và thực hiện cùng một việc, tiếp tục như vậy cho đến phần tử <html>.

Trong các trình duyệt hiện đại, mặc định tất cả các trình xử lý sự kiện được đăng ký trong giai đoạn bubbling. Vì vậy, trong ví dụ hiện tại, khi bạn nhấp vào video, sự kiện sủi bọt từ <video> sẽ chạy đến phần tử <html>. Cụ thể sẽ như sau:

  • Nó tìm video.onclick... và chạy nó, vì vậy trước tiên video sẽ bắt đầu chạy.
  • Sau đó tìm videoBox.onclick... và chạy nó, vì vậy video sẽ bị ẩn.

2.4. Sửa lỗi với stopPropagation()

Đây là một hành vi gây khó chịu nhưng có một cách để sửa nó. Đối tượng Sự kiện chuẩn có một hàm được sử dụng là stopPropagation(), khi được gọi trong bộ xử lý sự kiện của đối tượng sự kiện, bộ xử lý đầu tiên sẽ chạy, nhưng sự kiện sẽ không sủi bọt một lần nữa, vì vậy không có các bộ xử lý khác sẽ được chạy:

video.onclick = function(e) {
  e.stopPropagation();
  video.play();
};

Lưu ý: Tại sao chúng ta quan tâm đến từng capturing và bubbling? Ý tưởng mà sữ dụng ít tương thích hơn như chúng ta có bây giờ, Netscape chỉ sử dụng capturing events và Internet Explorer chỉ sử dụng bubbling events. Khi W3C quyết định cố gắng tiêu chuẩn hóa hành vi và đạt được sự đồng thuận, họ đã kết thúc với hệ thống này kết hợp cả hai, điều này là một trình duyệt hiện đại mà chúng ta có.

Lưu ý: Như đã đề cập ở trên, mặc định tất cả các trình xử lý sự kiện được đăng ký trong giai đoạn bubbling và điều này thường đúng trong hầu hết thời gian. Nếu bạn thực sự muốn đăng kí một sự kiện trong giai đoạn capturing, bạn có thể đăng ký bộ xử lý của mình bằng cách sử dụng `addEventListener()` và thiết lập tùy chọn thứ ba là `true`.

2.5. Sự ủy quyền sự kiện (Event Delegation)

Bubbling cũng cho phép chúng ta tận dụng lợi thế của sự ủy quyền sự kiện - khái niệm này là dựa trên một sự thực rằng nếu bạn muốn một đoạn mã chạy khi bạn click vào một số lượng lớn các phần tử con, bạn có thể đặt trình lắng nghe sự kiện cho cha của chúng và có sự kiện đó xảy ra trên chúng nổi bọt lên cha có thể phải đặt lắng nghe sự kiện ở mỗi con. Nhớ rằng trước đó chúng tôi đã nói rằng sự nổi bọt liên quan đến việc kiểm tra phần tử sự kiện được kích hoạt cho một trình xử lý sự kiện đầu tiên, sau đó di chuyển lên thành phần cha của nó?

Một ví dụ tốt là khi có nhiều mục danh sách - nếu bạn muốn từng mục hiển thị một tin nhắn khi nhấp vào, bạn có thể đặt lắng nghe sự kiện click trên thẻ cha <ul> và sự kiện sẽ nổi bọt từ mỗi mục con lên <ul>:

document.getElementById("parent-list").addEventListener("click", function(e) {
  // e.target is the clicked element!
  // If it was a list item
  if (e.target && e.target.nodeName == "LI") {
    // List item found! Output the ID!
    console.log("List item ", e.target.id.replace("post-", ""), " was clicked!");
  }
});

Bắt đầu bằng cách thêm trình nghe sự kiện nhấp chuột vào thẻ cha. Khi trình nghe sự kiện được kích hoạt, xác định phần tử sự kiện để đảm bảo rằng nó là loại phần tử được định sẵn. Nếu nó là LI element, boom: chính nó tôi đang tìm. Nếu không phải phần tử tôi cần, sự kiện sẽ bị bỏ qua. Đây chỉ là ví dụ đơn giản - <UL><LI> chỉ đơn giản là so sánh. Hãy thử một số thứ khó hơn. Hãy xem xét thẻ cha <DIV> có nhiều con, nhưng chúng ta chỉ quan tâm đến một nhãn A với lớp CSS classA:

document.getElementById("myDiv").addEventListener("click",function(e) {
  // e.target was the clicked element
  if (e.target && e.target.matches("a.classA")) {
    console.log("Đã nhấp vào phần tử nhãn");
  }
});

Bạn có thể xem API Element.matches tại trang Element.matches.

Vì hầu hết các nhà phát triển sử dụng thư viện JavaScript để xử lý sự kiện và các thành phần DOM của họ, tôi khuyên bạn nên sử dụng phương pháp ủy quyền sự kiện của thư viện, vì chúng đều có khả năng xác định ủy nhiệm và phần tử nâng cao.

Hy vọng bài viết này hữu ích cho bạn về những khái niệm xung quanh sự ủy quyền sự kiện và sự tiện ích xung quanh sức mạnh của sự ủy quyền này.

  • Có 3 cách chung để gán sự kiện cho một phần tử DOM: gán vào thuộc tính, gán trực tiếp hoặc sử dụng addEventListener().
  • Cách addEventListener() có thể gán nhiều sự kiện cho một phần tử và có một đối tác là removeEventListener() để loại bỏ hiệu quả sự kiện đã gán.
  • Ngoài ra khi truyền sự kiện cũng có nhiều đối tượng được truyền.
  • Bạn nên hiểu hành vi "bubbling" và "capture" trong sự kiện, các DOM xử lý sự kiện theo cha, con. Bên cạnh đó tận dụng lợi thế của nó với Event delegation.
1