Hôm nay cafedev chia sẻ cho ace sâu vào chi tiết hơn về các sự kiện xảy ra khi chuột di chuyển giữa các phần tử.

1. Sự kiện di chuột qua / di chuột ra, liên quan

Sự kiện mouseover xảy ra khi con trỏ chuột đi qua một phần tử và mouseout- khi nó rời đi.

Những sự kiện này là đặc biệt, bởi vì chúng có thuộc tính relatedTarget. Thuộc tính này bổ sung target. Khi một con chuột rời khỏi một phần tử này cho phần tử khác, một trong số chúng sẽ trở thành target, và phần tử kia – relatedTarget.

Đối với mouseover:

  • event.target – là phần tử mà con chuột đã lướt qua.
  • event.relatedTarget- là phần tử mà từ đó chuột đến ( relatedTarget→ target).

Đối với mouseout ngược lại:

  • event.target – là phần tử mà chuột để lại.
  • event.relatedTarget- là phần tử mới dưới con trỏ, chuột để lại cho ( target→ relatedTarget).

Trong ví dụ bên dưới, mỗi khuôn mặt và các đặc điểm của nó là các yếu tố riêng biệt. Khi bạn di chuyển chuột, bạn có thể thấy các sự kiện chuột trong vùng văn bản.

Mỗi sự kiện có thông tin về cả hai target và relatedTarget

relatedTarget có thể null

Các thuộc tính relatedTarget có thể được null.

Đó là điều bình thường và có nghĩa là con chuột không đến từ một phần tử khác, mà là từ ngoài cửa sổ. Hoặc nó rời khỏi cửa sổ.

Chúng ta nên ghi nhớ khả năng đó khi sử dụng code event.relatedTarget của mình. Nếu chúng ta truy cập event.relatedTarget.tagName thì sẽ xảy ra lỗi.

2. Bỏ qua các phần tử

Sự kiện mousemove kích hoạt khi chuột di chuyển. Nhưng điều đó không có nghĩa là mọi pixel đều dẫn đến một sự kiện.

Trình duyệt sẽ kiểm tra vị trí chuột theo thời gian. Và nếu nó nhận thấy những thay đổi thì sẽ kích hoạt các sự kiện.

Điều đó có nghĩa là nếu khách truy cập di chuyển chuột rất nhanh thì một số phần tử DOM có thể bị bỏ qua:

Nếu chuột di chuyển rất nhanh từ #FROM đến #TO các phần tử như đã vẽ ở trên, thì các phần tử <div> trung gian (hoặc một số trong số chúng) có thể bị bỏ qua. Các sự kiện mouseout có thể kích hoạt trên #FROM và sau đó ngay lập tức mouseover trên #TO.

Điều đó tốt cho hiệu suất, vì có thể có nhiều yếu tố trung gian. Chúng ta không thực sự muốn xử lý trong và ngoài từng cái.

Mặt khác, chúng ta nên lưu ý rằng con trỏ chuột không “ghé thăm” tất cả các phần tử trên đường đi. Nó có thể nhảy”.

Đặc biệt, có thể con trỏ nhảy ngay bên trong giữa trang từ ngoài cửa sổ. Trong trường hợp đó relatedTarget là null, bởi vì nó đến từ “một nơi không xác định”:

Bạn có thể kiểm tra nó “trực tiếp” trên một quầy thử nghiệm bên dưới.

HTML của nó có hai phần tử lồng nhau: phần tử <div id="child"> nằm bên trong <div id="parent">. Nếu bạn di chuyển chuột nhanh qua chúng, thì có thể chỉ div con mới kích hoạt các sự kiện, hoặc có thể là div cha, hoặc có thể sẽ không có sự kiện nào cả.

Đồng thời di chuyển con trỏ vào con div, rồi nhanh chóng di chuyển con trỏ xuống dưới qua con trỏ . Nếu chuyển động đủ nhanh, thì phần tử cha bị bỏ qua. Chuột sẽ vượt qua phần tử mẹ mà không nhận thấy nó.

Nếu mouseover được kích hoạt, phải có mouseout

Trong trường hợp di chuyển chuột nhanh, các phần tử trung gian có thể bị bỏ qua, nhưng chúng ta biết chắc một điều: nếu con trỏ “chính thức” nhập vào một phần tử ( sự kiện mouseover được tạo), thì khi rời khỏi nó, chúng ta luôn nhận được mouseout.

3. Di chuột khi để một phần tử con

Một tính năng quan trọng của mouseout- nó kích hoạt, khi con trỏ di chuyển từ một phần tử sang phần tử con của nó, ví dụ: từ #parent đến #child trong HTML này:

<div id="parent">
  <div id="child">...</div>
</div>

Nếu chúng ta đang ở trên #parent và sau đó di chuyển con trỏ vào sâu hơn #child, nhưng chúng ta sẽ tiếp tục mouseout #parent!

Điều đó có vẻ kỳ lạ, nhưng có thể dễ dàng giải thích.

Theo logic của trình duyệt, con trỏ chuột có thể chỉ ở trên một phần tử duy nhất vào bất kỳ lúc nào – phần tử lồng nhau nhất và trên cùng theo chỉ mục z.

Vì vậy, nếu nó đi đến một phần tử khác (thậm chí là một phần tử con), thì nó sẽ rời khỏi phần tử trước đó.

Xin lưu ý một chi tiết quan trọng khác của quá trình xử lý sự kiện.

Sự kiện mouseover trên một phần tử con đang sủi bọt. Vì vậy, nếu #parent có trình xử lý mouseover, nó sẽ kích hoạt:

Bạn có thể thấy điều đó rất rõ trong ví dụ bên dưới: <div id="child"> bên trong <div id="parent">. Có các trình xử lý mouseover/out trên phần tử #parent xuất chi tiết sự kiện.

Nếu bạn di chuyển chuột từ #parent đến #child, bạn thấy hai sự kiện trên #parent:

  1. mouseout [target: parent] (rời khỏi cha mẹ), sau đó
  2. mouseover [target: child] (đến con, sủi bọt).

Như được minh họa, khi con trỏ di chuyển từ phần tử #parent sang #child, hai trình xử lý sẽ kích hoạt trên phần tử mẹ: mouseout và mouseover:

parent.onmouseout = function(event) {
  /* event.target: parent element */
};
parent.onmouseover = function(event) {
  /* event.target: child element (bubbled) */
};

Nếu chúng ta không kiểm tra event.target bên trong các trình xử lý, thì có vẻ như con trỏ chuột đã rời phần tử #parent và sau đó ngay lập tức quay lại nó.

Nhưng không phải vậy đâu! Con trỏ vẫn ở trên cha, nó chỉ di chuyển sâu hơn vào phần tử con.

Nếu có một số hành động khi rời khỏi phần tử mẹ, ví dụ như một hoạt ảnh chạy vào parent.onmouseout, chúng ta thường không muốn nó khi con trỏ đi sâu hơn vào #parent.

Để tránh điều này, chúng ta có thể kiểm tra trình xử lý relatedTarget và nếu chuột vẫn ở bên trong phần tử thì hãy bỏ qua sự kiện đó.

Ngoài ra, chúng ta có thể sử dụng các sự kiện khác: mouseenter và mouseleave, chúng ta sẽ đề cập ngay bây giờ, vì chúng không gặp vấn đề như vậy.

4. Sự kiện mouseenter và mouseleave

Sự kiện mouseenter/mouseleave là như thế nào với  mouseover/mouseout. Chúng kích hoạt khi con trỏ chuột đi vào / rời khỏi phần tử.

Nhưng có hai điểm khác biệt quan trọng:

  1. Các chuyển đổi bên trong phần tử, đến / từ phần tử con cháu, không được tính.
  2. Sự kiện mouseenter/mouseleave không có sủi bọt.

Những sự kiện này cực kỳ đơn giản.

Khi con trỏ đi vào một phần tử – mouseenter sẽ kích hoạt. Vị trí chính xác của con trỏ bên trong phần tử hoặc con trỏ của nó không quan trọng.

Khi con trỏ rời khỏi một phần tử – mouseleave sẽ kích hoạt.

Ví dụ này tương tự như ở trên, nhưng bây giờ phần tử trên cùng có mouseenter/mouseleave thay vì mouseover/mouseout.

Như bạn có thể thấy, các sự kiện được tạo duy nhất là những sự kiện liên quan đến việc di chuyển con trỏ vào và ra khỏi phần tử trên cùng. Không có gì xảy ra khi con trỏ đi tới con và quay lại. Chuyển đổi giữa các con cháu bị bỏ qua

5. Uỷ quyền sự kiện(Event delegation)

Sự kiện mouseenter/leave rất đơn giản và dễ sử dụng. Nhưng chúng không có cơ chế sủi bọt. Vì vậy, chúng ta không thể sử dụng ủy quyền sự kiện với họ.

Hãy tưởng tượng chúng ta muốn xử lý nhập / rời chuột cho các ô trong bảng. Và có hàng trăm ô.

Giải pháp tự nhiên sẽ là – đặt trình xử lý <table>và xử lý các sự kiện ở đó. Nhưng mouseenter/leave không có cơ chế sủi bọt. Vì vậy, nếu sự kiện như vậy xảy ra vào <td>, thì chỉ một trình xử lý trên <td> đó mới có thể nắm bắt được nó.

Trình xử lý cho mouseenter/leave vào <table>chỉ kích hoạt khi con trỏ đi vào / rời khỏi bảng nói chung. Không thể lấy bất kỳ thông tin nào về quá trình chuyển đổi bên trong nó.

Vì vậy, hãy sử dụng mouseover/mouseout.

Hãy bắt đầu với các trình xử lý đơn giản làm nổi bật phần tử dưới chuột:

// let's highlight an element under the pointer
table.onmouseover = function(event) {
  let target = event.target;
  target.style.background = 'pink';
};

table.onmouseout = function(event) {
  let target = event.target;
  target.style.background = '';
};

Đây là họ đang hoạt động. Khi chuột di chuyển qua các phần tử của bảng này, phần tử hiện tại được tô sáng:

Trong trường hợp của chúng ta, chúng ta muốn xử lý chuyển đổi giữa các ô trong bảng <td>: nhập một ô và rời khỏi ô đó. Các chuyển đổi khác, chẳng hạn như bên trong ô hoặc bên ngoài bất kỳ ô nào, chúng ta không quan tâm. Hãy lọc chúng ra.

Đây là những gì chúng tôi có thể làm:

  • Hãy nhớ hiện tại được đánh dấu <td> trong một biến, hãy gọi nó currentElem.
  • Bật mouseover- bỏ qua sự kiện nếu chúng ta vẫn ở trong hiện tại <td>.
  • Bật mouseout – bỏ qua nếu chúng ta không để lại hiện tại <td>.

Dưới đây là một ví dụ về code giải thích cho tất cả các trường hợp có thể xảy ra:

// <td> under the mouse right now (if any)
let currentElem = null;

table.onmouseover = function(event) {
  // before entering a new element, the mouse always leaves the previous one
  // if currentElem is set, we didn't leave the previous <td>,
  // that's a mouseover inside it, ignore the event
  if (currentElem) return;

  let target = event.target.closest('td');

  // we moved not into a <td> - ignore
  if (!target) return;

  // moved into <td>, but outside of our table (possible in case of nested tables)
  // ignore
  if (!table.contains(target)) return;

  // hooray! we entered a new <td>
  currentElem = target;
  onEnter(currentElem);
};


table.onmouseout = function(event) {
  // if we're outside of any <td> now, then ignore the event
  // that's probably a move inside the table, but out of <td>,
  // e.g. from <tr> to another <tr>
  if (!currentElem) return;

  // we're leaving the element – where to? Maybe to a descendant?
  let relatedTarget = event.relatedTarget;

  while (relatedTarget) {
    // go up the parent chain and check – if we're still inside currentElem
    // then that's an internal transition – ignore it
    if (relatedTarget == currentElem) return;

    relatedTarget = relatedTarget.parentNode;
  }

  // we left the <td>. really.
  onLeave(currentElem);
  currentElem = null;
};

// any functions to handle entering/leaving an element
function onEnter(elem) {
  elem.style.background = 'pink';

  // show that in textarea
  text.value += `over -> ${currentElem.tagName}.${currentElem.className}\n`;
  text.scrollTop = 1e6;
}

function onLeave(elem) {
  elem.style.background = '';

  // show that in textarea
  text.value += `out <- ${elem.tagName}.${elem.className}\n`;
  text.scrollTop = 1e6;
}

Một lần nữa, các tính năng quan trọng là:

  1. Nó sử dụng ủy quyền sự kiện để xử lý việc vào / rời bất kỳ <td> bên trong bảng nào. Vì vậy, nó dựa vào mouseover/out thay vì mouseenter/leave không có cơ chế sủi bọt và do đó không cho phép ủy quyền.
  2. Các sự kiện bổ sung, chẳng hạn như di chuyển giữa các phần tử con của <td> được lọc ra, do đó onEnter/Leave chỉ chạy khi con trỏ rời đi hoặc đi vào toàn bộ <td>.

Cố gắng di chuyển con trỏ vào và ra khỏi các ô của bảng và bên trong chúng. Nhanh hay chậm – không quan trọng. Chỉ toàn bộ <td> được đánh dấu, không giống như ví dụ trước.

6. Tóm lược

Chúng bao gồm các sự kiện mouseover, mouseout, mousemove, mouseenter và mouseleave.

Những điều này cần lưu ý:

  • Di chuyển chuột nhanh có thể bỏ qua các yếu tố trung gian.
  • Sự kiện mouseover/out và mouseenter/leave có một thêm thuộc tính: relatedTarget. Đó là yếu tố mà chúng ta đến từ / đi từ, bổ sung cho nhau.

Sự kiện mouseover/out kích hoạt ngay cả khi chúng ta đi từ phần tử mẹ sang phần tử con. Trình duyệt giả định rằng chuột chỉ có thể ở trên một phần tử tại một thời điểm – phần tử sâu nhất.

Các sự kiện mouseenter/leave khác nhau ở khía cạnh đó: chúng chỉ kích hoạt khi chuột vào và ra toàn bộ phần tử. Ngoài ra chúng không có cơ chế sủi bọt.

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!