Cơ chế bắt giữ và sủi bọt cho phép chúng ta triển khai một trong những mẫu xử lý sự kiện mạnh mẽ nhất được gọi là ủy quyền sự kiện(Event delegation). Hôm nay, cafedev chia sẻ cho ace về cơ chế ủy quyền sự kiện(Event delegation).

Ý tưởng là nếu chúng ta có nhiều phần tử được xử lý theo cách tương tự, thì thay vì gán một trình xử lý cho từng phần tử – chúng ta đặt một trình xử lý duy nhất trên tổ tiên chung của chúng.

Trong trình xử lý mà chúng ta nhận được event.target, hãy xem nơi thực sự xảy ra sự kiện và xử lý nó.

HTML giống như sau:

<table>
  <tr>
    <th colspan="3"><em>Bagua</em> Chart: Direction, Element, Color, Meaning</th>
  </tr>
  <tr>
    <td class="nw"><strong>Northwest</strong><br>Metal<br>Silver<br>Elders</td>
    <td class="n">...</td>
    <td class="ne">...</td>
  </tr>
  <tr>...2 more lines of this kind...</tr>
  <tr>...2 more lines of this kind...</tr>
</table>

Bảng có 9 ô, nhưng có thể có 99 hoặc 9999, không quan trọng.

Nhiệm vụ của chúng ta là đánh dấu một ô <td> khi nhấp chuột.

Thay vì chỉ định một trình xử lý onclick cho mỗi <td>(có thể là nhiều) – chúng ta sẽ thiết lập trình xử lý “bắt tất cả” trên phần tử <table>.

Nó sẽ sử dụng event.target để lấy phần tử được nhấp và đánh dấu nó.

Code:

let selectedTd;

table.onclick = function(event) {
  let target = event.target; // where was the click?

  if (target.tagName != 'TD') return; // not on TD? Then we're not interested

  highlight(target); // highlight it
};

function highlight(td) {
  if (selectedTd) { // remove the existing highlight if any
    selectedTd.classList.remove('highlight');
  }
  selectedTd = td;
  selectedTd.classList.add('highlight'); // highlight the new td
}

Code như vậy không quan tâm có bao nhiêu ô trong bảng. Chúng ta có thể thêm / xóa <td> động bất kỳ lúc nào và phần đánh dấu vẫn hoạt động.

Tuy nhiên, có một nhược điểm.

Nhấp chuột có thể xảy ra không phải trên <td>, nhưng bên trong nó.

Trong trường hợp của chúng ta, nếu chúng ta nhìn vào bên trong HTML, chúng ta có thể thấy các thẻ lồng nhau bên trong <td>, như <strong>:

<td>
  <strong>Northwest</strong>
  ...
</td>

Đương nhiên, nếu một nhấp chuột xảy ra trên đó <strong> thì nó sẽ trở thành giá trị của event.target.

Trong trình xử lý, table.onclick chúng ta nên lấy như vậy từ event.target và tìm hiểu xem liệu nhấp chuột có bên trong <td> hay không.

Đây là code được cải thiện:

table.onclick = function(event) {
  let td = event.target.closest('td'); // (1)

  if (!td) return; // (2)

  if (!table.contains(td)) return; // (3)

  highlight(td); // (4)
};

Giải thích:

  1. Phương thức elem.closest(selector) trả về tổ tiên gần nhất phù hợp với bộ chọn. Trong trường hợp của chúng ta, chúng ta tìm <td> cách đi lên từ phần tử nguồn.
  2. Nếu event.target không có bên trong bất kỳ <td>, thì cuộc gọi sẽ trở lại ngay lập tức, vì không có gì phải làm.
  3. Trong trường hợp các bảng lồng nhau, event.target có thể là một <td>, nhưng nằm ngoài bảng hiện tại. Vì vậy, chúng ta kiểm tra xem đó có thực sự là bàn của chúng ta không <td> .
  4. Và, nếu đúng như vậy, thì hãy làm nổi bật nó.

Kết quả là chúng ta có một mã đánh dấu hiệu quả, nhanh chóng mà không quan tâm đến tổng số <td> trong bảng.

1. Ví dụ về ủy quyền: các hành động trong đánh dấu

Có những cách sử dụng khác cho việc ủy ​​quyền sự kiện.

Giả sử, chúng ta muốn tạo một menu với các nút “Lưu”, “Tải”, “Tìm kiếm”, v.v. Và có một đối tượng với phương pháp save, load, search… Làm thế nào để phù hợp với họ?

Ý tưởng đầu tiên có thể là gán một trình xử lý riêng cho mỗi nút. Nhưng có một giải pháp hay hơn. Chúng ta có thể thêm một trình xử lý cho toàn bộ menu và các thuộc tính data-action cho các nút có phương thức gọi:

<button data-action="save">Click to Save</button>

Trình xử lý đọc thuộc tính và thực thi phương thức. Hãy xem ví dụ làm việc:

<div id="menu">
  <button data-action="save">Save</button>
  <button data-action="load">Load</button>
  <button data-action="search">Search</button>
</div>

<script>
  class Menu {
    constructor(elem) {
      this._elem = elem;
      elem.onclick = this.onClick.bind(this); // (*)
    }

    save() {
      alert('saving');
    }

    load() {
      alert('loading');
    }

    search() {
      alert('searching');
    }

    onClick(event) {
      let action = event.target.dataset.action;
      if (action) {
        this[action]();
      }
    };
  }

  new Menu(menu);
</script>

Xin lưu ý rằng this.onClick bị ràng buộc this trong (*). Điều đó quan trọng, bởi vì nếu không thì this bên trong nó sẽ tham chiếu đến phần tử DOM ( elem), không phải đối tượng Menu và this[action] sẽ không phải là thứ chúng ta cần.

Vậy, ủy quyền mang lại cho chúng ta những lợi thế gì?

  • Chúng ta không cần phải viết code để gán một trình xử lý cho mỗi nút. Chỉ cần tạo một phương thức và đặt nó vào phần đánh dấu.
  • Cấu trúc HTML rất linh hoạt, chúng ta có thể thêm / bớt các nút bất cứ lúc nào.

Chúng ta cũng có thể sử dụng các lớp học .action-save, .action-load nhưng một thuộc tính data-action là tốt hơn về ngữ nghĩa. Và chúng ta cũng có thể sử dụng nó trong các quy tắc CSS.

2. Mẫu “hành vi”

Chúng ta cũng có thể sử dụng ủy quyền sự kiện để thêm “hành vi” vào các phần tử một cách khai báo , với các thuộc tính và lớp đặc biệt.

Mô hình có hai phần:

  1. Chúng ta thêm một thuộc tính tùy chỉnh vào một phần tử mô tả hành vi của nó.
  2. Trình xử lý trên toàn tài liệu theo dõi các sự kiện và nếu sự kiện xảy ra trên một phần tử được quy – thực hiện hành động.

2.1. Hành vi: Truy cập

Ví dụ: ở đây thuộc tính data-counter thêm một hành vi: “tăng giá trị khi nhấp chuột” vào các nút:

Counter: <input type="button" value="1" data-counter>
One more counter: <input type="button" value="2" data-counter>

<script>
  document.addEventListener('click', function(event) {

    if (event.target.dataset.counter != undefined) { // if the attribute exists...
      event.target.value++;
    }

  });
</script>

Nếu chúng ta nhấp vào một nút – giá trị của nó sẽ tăng lên. Không phải các nút, nhưng cách tiếp cận chung là quan trọng ở đây.

Có thể có bao nhiêu thuộc tính data-counter tùy thích. Chúng ta có thể thêm những cái mới vào HTML bất kỳ lúc nào. Bằng cách sử dụng ủy quyền sự kiện, chúng ta đã “mở rộng” HTML, đã thêm một thuộc tính mô tả một hành vi mới.

Đối với trình xử lý cấp tài liệu – luôn luôn addEventListener

Khi chúng ta chỉ định một trình xử lý sự kiện cho đối tượng document, chúng ta nên luôn sử dụng addEventListener, không nên sử dụng document.on<event>, bởi vì trình xử lý sau sẽ gây ra xung đột: trình xử lý mới ghi đè lên những trình xử lý cũ.

Đối với các dự án thực, điều bình thường là có nhiều trình xử lý document được thiết lập bởi các phần khác nhau của code.

2.2. Hành vi: Toggler

Thêm một ví dụ về hành vi. Một cú nhấp chuột vào một phần tử có thuộc tính data-toggle-id sẽ hiển thị / ẩn phần tử với giá trị đã cho id:

/*
Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
@author cafedevn
Contact: cafedevn@gmail.com
Fanpage: https://www.facebook.com/cafedevn
Group: https://www.facebook.com/groups/cafedev.vn/
Instagram: https://instagram.com/cafedevn
Twitter: https://twitter.com/CafedeVn
Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
Pinterest: https://www.pinterest.com/cafedevvn/
YouTube: https://www.youtube.com/channel/UCE7zpY_SlHGEgo67pHxqIoA/
*/

<button data-toggle-id="subscribe-mail">
  Show the subscription form
</button>

<form id="subscribe-mail" hidden>
  Your mail: <input type="email">
</form>

<script>
  document.addEventListener('click', function(event) {
    let id = event.target.dataset.toggleId;
    if (!id) return;

    let elem = document.getElementById(id);

    elem.hidden = !elem.hidden;
  });
</script>

Hãy ghi nhận một lần nữa những gì chúng ta đã làm. Bây giờ, để thêm chức năng chuyển đổi vào một phần tử – không cần biết JavaScript, chỉ cần sử dụng thuộc tính data-toggle-id.

Điều đó có thể trở nên thực sự thuận tiện – không cần phải viết JavaScript cho mọi phần tử như vậy. Chỉ sử dụng hành vi. Trình xử lý cấp tài liệu làm cho nó hoạt động cho bất kỳ phần tử nào của trang.

Chúng ta cũng có thể kết hợp nhiều hành vi trên một phần tử.

Mẫu “hành vi” có thể là một thay thế cho các đoạn JavaScript nhỏ.

3. Tóm lược

ủy quyền sự kiện thật là tuyệt! Đây là một trong những mẫu hữu ích nhất cho các sự kiện DOM.

Nó thường được sử dụng để thêm cùng một cách xử lý cho nhiều phần tử tương tự, nhưng không chỉ cho điều đó.

Thuật toán:

  1. Đặt một trình xử lý duy nhất trên thùng chứa.
  2. Trong trình xử lý – kiểm tra phần tử nguồn event.target.
  3. Nếu sự kiện xảy ra bên trong một phần tử mà chúng ta quan tâm, thì hãy xử lý sự kiện đó.

Những lợi ích:

  • Đơn giản hóa việc khởi tạo và tiết kiệm bộ nhớ: không cần thêm nhiều trình xử lý.
  • Ít code hơn: khi thêm hoặc bớt phần tử, không cần thêm / bớt phần xử lý.
  • Sửa đổi DOM: chúng ta có thể thêm / xóa hàng loạt các phần tử có innerHTML và tương tự.

Tất nhiên, delegation có những hạn chế:

  • Đầu tiên, sự kiện phải sôi sục. Một số sự kiện không sủi bọt . Ngoài ra, những người xử lý cấp thấp không nên sử dụng event.stopPropagation().
  • Thứ hai, ủy quyền có thể thêm tải cho CPU, bởi vì trình xử lý cấp vùng chứa phản ứng trên các sự kiện ở bất kỳ vị trí nào của vùng chứa, bất kể chúng có quan tâm đến chúng ta hay không. Nhưng thường thì tải không đáng kể nên chúng ta không tính đến.

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!

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!

Đăng ký kênh youtube để ủng hộ Cafedev nha các bạn, Thanks you!