Trên trang Cafedev, chúng ta sẽ khám phá cách JavaScript xử lý các sự kiện liên quan đến việc di chuyển chuột như `mouseover`, `mouseout`, `mouseenter`, và `mouseleave`. Những sự kiện này cho phép bạn theo dõi và phản hồi hành vi của người dùng khi chuột di chuyển qua hoặc ra khỏi các phần tử trên trang. Bằng cách nắm vững những sự kiện này, bạn có thể tạo ra các hiệu ứng tương tác mượt mà và tinh tế, nâng cao trải nghiệm người dùng trên trang web của bạn.

Hãy đi sâu vào chi tiết 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 mouseover/mouseout, relatedTarget

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

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

Đối với mouseover:

  • event.target — là phần tử mà chuột đã di chuyển qua.
  • event.relatedTarget — là phần tử từ đó chuột đã di chuyển đến (relatedTarget -> target).


Đối với mouseout, điều ngược lại:

  • event.target — là phần tử mà chuột đã rời khỏi.
  • event.relatedTarget — là phần tử mới dưới con trỏ chuột, nơi chuột đã rời đi (target -> relatedTarget).

Trong ví dụ dưới đây, mỗi mặt và các đặc điểm của nó là các phần 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 khu vực văn bản.
Mỗi sự kiện đều có thông tin về cả targetrelatedTarget:

Link demo

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

Chúng ta nên giữ khả năng này trong tâm trí khi sử dụng event.relatedTarget trong mã của mình. Nếu chúng ta truy cập event.relatedTarget.tagName, sẽ có lỗi xảy ra.

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

Sự kiện mousemove xảy ra khi chuột di chuyển. Nhưng điều đó không có nghĩa là mỗi pixel di chuyển đều dẫn đến một sự kiện.
Trình duyệt kiểm tra vị trí chuột từ thời gian này sang thời gian khác. Và nếu nó phát hiện thấy sự thay đổi thì sẽ kích hoạt các sự kiện.

Điều đó có nghĩa là nếu người dùng 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ừ phần tử #FROM đến phần tử #TO như trong hình minh họa ở trên, thì các phần tử <div> trung gian (hoặc một số trong chúng) có thể bị bỏ qua. 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 này tốt cho hiệu suất, vì có thể có nhiều phần tử trung gian. Chúng ta không thực sự muốn xử lý sự kiện vào và ra của từng phần tử.
Mặt khác, chúng ta nên lưu ý rằng con trỏ chuột không “thăm” tất cả các phần tử trên đường đi. Nó có thể “nhảy” qua.

Cụ thể, có thể con trỏ nhảy ngay vào giữa trang từ ngoài cửa sổ. Trong trường hợp đó, relatedTargetnull, vì nó đến từ “không đâu cả”:


Bạn có thể kiểm tra “trực tiếp” trên bảng thử nghiệm dưới đây.

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

Cũng hãy di chuyển con trỏ vào div con, rồi nhanh chóng di chuyển ra ngoài qua phần tử cha. Nếu di chuyển đủ nhanh, thì phần tử cha sẽ bị bỏ qua. Chuột sẽ đi qua phần tử cha mà không nhận ra.

Link demo

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 có một điều chắc chắn: nếu con trỏ “chính thức” vào một phần tử (mouseover được kích hoạt), thì khi rời khỏi nó, chúng ta luôn nhận được mouseout.

3. Mouseout khi rời sang phần tử con

Một đặc điểm quan trọng của mouseout — nó được 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 ở trên #parent và sau đó di chuyển con trỏ sâu vào #child, chúng ta sẽ nhận được sự kiện mouseout trên #parent!



Điều này có thể có vẻ lạ, nhưng có thể dễ dàng giải thích.
Theo logic của trình duyệt, con trỏ chuột chỉ có thể nằm trên một phần tử duy nhất tại một thời điểm — phần tử lồng ghép nhất và nằm trên cùng theo chỉ số z.

Vì vậy, nếu con trỏ chuột di chuyển đến một phần tử khác (ngay cả phần tử con), thì nó sẽ rời khỏi phần tử trước đó.
Hãy lưu ý một chi tiết quan trọng khác của việc xử lý sự kiện.

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


Nếu bạn di chuyển chuột từ #parent đến #child, bạn sẽ thấy hai sự kiện trên #parent:
1. mouseout [target: parent] (rời khỏi phần tử cha), sau đó
2. mouseover [target: child] (đi đến phần tử con, nổi bọt lên).

Link demo


Như được hiển thị, khi con trỏ di chuyển từ phần tử #parent đến #child, hai trình xử lý sẽ được kích hoạt trên phần tử cha: mouseoutmouseover:

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ó thể có vẻ như con trỏ chuột đã rời khỏi phần tử #parent, và sau đó ngay lập tức quay trở lại trên nó.
Nhưng đó không phải là trường hợp! Con trỏ vẫn ở trên phần tử cha, chỉ là nó 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ử cha, ví dụ như một hoạt ảnh chạy trong parent.onmouseout, chúng ta thường không muốn điều đó khi con trỏ chỉ đi sâu hơn vào #parent.

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

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

4. Các sự kiện mouseenter và mouseleave

Các sự kiện mouseenter/mouseleave giống như mouseover/mouseout. Chúng được kích hoạt khi con trỏ chuột vào/xuất ra khỏi phần tử.
Nhưng có hai khác biệt quan trọng:

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

Các sự kiện này cực kỳ đơn giản.

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

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

Ví dụ này tương tự như ví dụ trên, nhưng giờ đây 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 sinh ra chỉ 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ỏ di chuyển vào phần tử con và quay lại. Các chuyển tiếp giữa các phần tử con bị bỏ qua.

Link Demo

5. Phân phối sự kiện

Các sự kiện mouseenter/leave rất đơn giản và dễ sử dụng. Nhưng chúng không nổi bọt. Vì vậy, chúng ta không thể sử dụng phân phối sự kiện với chúng.
Giả sử chúng ta muốn xử lý sự kiện con chuột vào/ra khỏi các ô trong bảng. Và có hàng trăm ô.

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

Các trình xử lý cho mouseenter/leave trên <table> chỉ được kích hoạt khi con trỏ vào/ra khỏi toàn bộ bảng. Không thể lấy thông tin về các chuyển tiếp bên trong bảng.

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

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à chúng trong hành độ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 làm nổi bật:

Link Demo

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

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

  • Ghi nhớ phần tử <td> hiện tại đang được làm nổi bật trong một biến, gọi là currentElem.
  • Trong sự kiện mouseover — bỏ qua sự kiện nếu chúng ta vẫn ở bên trong <td> hiện tại.
  • Trong sự kiện mouseout — bỏ qua nếu chúng ta chưa rời khỏi <td> hiện tại.


Đây là ví dụ mã code tính đến tất cả các tình huống 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à:

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

    Đây là ví dụ đầy đủ với tất cả các chi tiết:

    Link demo

    Hãy thử di chuyển con trỏ vào và ra khỏi các ô trong bảng và bên trong chúng. Nhanh hay chậm — không quan trọng. Chỉ có <td> như một tổng thể được làm nổi bật, khác với ví dụ trước.

    6. Tóm tắt

    Chúng ta đã đề cập đến các sự kiện mouseover, mouseout, mousemove, mouseentermouseleave.
    Các điểm cần lưu ý:

    • Một chuyển động chuột nhanh có thể bỏ qua các phần tử trung gian.
    • Các sự kiện mouseover/outmouseenter/leave có thuộc tính bổ sung: relatedTarget. Đây là phần tử mà chúng ta đến từ hoặc đến, bổ sung cho target.
      Các sự kiện mouseover/out được kích hoạt ngay cả khi chúng ta đi từ phần tử cha sang phần tử con. Trình duyệt giả định rằng chuột chỉ có thể ở 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 ở điểm này: chúng chỉ được kích hoạt khi chuột vào và ra khỏi phần tử như một tổng thể. Đồng thời, chúng không nổi bọt.

    Hy vọng rằng qua bài viết trên, bạn đã có cái nhìn rõ hơn về các sự kiện chuột trong JavaScript như `mouseover`, `mouseout`, `mouseenter`, và `mouseleave`. Những sự kiện này cung cấp những công cụ mạnh mẽ để tạo ra các tương tác mượt mà và hiệu quả. Cafedev luôn đồng hành cùng bạn trong hành trình học lập trình. Tiếp tục khám phá và áp dụng kiến thức vào các dự án của bạn để nâng cao kỹ năng và trải nghiệm người dùng trên trang web của bạn.

    Tham khảo thêm: MIỄN PHÍ 100% | Series tự học Javascrypt chi tiết, dễ hiểu từ cơ bản tới nâng cao + Full Bài Tập thực hành nâng cao trình dev

    Các nguồn kiến thức MIỄN PHÍ VÔ GIÁ từ cafedev tại đây

    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!