mouseenter與mouseover爲什麼這般糾纏不清?

前言

原文地址javascript

項目地址css

不知道你們在面試或者工做過程當中有沒有被mouseovermouseenter(對應的是mouseoutmouseleave)事件所困擾。本身以前在面試的時候就有被問到諸如mouseover和mouseenter事件的異同之類的問題?當時沒有答出來,一直也對這兩個事件有點模糊不清,趁着最近正在讀zepto源碼,準備寫一篇這方面的文章,若是有錯誤,請你們指正。html

<!--more-->java

mouseenter與mouseover的異同?

要說清楚mouseenter與mouseover有什麼不一樣,也許能夠從兩方面去講。node

  1. 是否支持冒泡git

  2. 事件的觸發時機github

先來看一張圖,對這兩個事件有一個簡單直觀的感覺。面試

再看看官網對mouseenter的解釋api

mouseenter | onmouseenter event.aspx)瀏覽器

The event fires only if the mouse pointer is outside the boundaries of the object and the user moves the mouse pointer inside the boundaries of the object. If the mouse pointer is currently inside the boundaries of the object, for the event to fire, the user must move the mouse pointer outside the boundaries of the object and then back inside the boundaries of the object.

大概意思是說:當鼠標從元素的邊界以外移入元素的邊界以內時,事件被觸發。而當鼠標自己在元素邊界內時,要觸發該事件,必須先將鼠標移出元素邊界外,再次移入才能觸發。(英語比較渣?,湊合看哈)

Unlike the onmouseover event, the onmouseenter event does not bubble.

大概意思是:和mouseover不一樣的是,mouseenter不支持事件冒泡 (英語比較渣?,湊合看哈)

因爲mouseenter不支持事件冒泡,致使在一個元素的子元素上進入或離開的時候會觸發其mouseover和mouseout事件,可是卻不會觸發mouseenter和mouseleave事件

咱們用一張動圖來看看他們的區別(或者點擊該連接體驗)。

咱們給左右兩邊的ul分別添加了mouseovermouseenter事件,當鼠標進入左右兩邊的ul時,mouseovermouseenter事件都觸發了,可是當移入各自的子元素li的時候,觸發了左邊ul上的mouseover事件,然而右邊ul的mouseenter事件沒有被觸發。

形成以上現象本質上是mouseenter事件不支持冒泡所致。

如何模擬mouseenter事件。

可見mouseover事件因其具備冒泡的性質,在子元素內移動的時候,頻繁被觸發,若是咱們不但願如此,可使用mouseenter事件代替之,可是早期只有ie瀏覽器支持該事件,雖然如今大多數高級瀏覽器都支持了mouseenter事件,可是不免會有些兼容問題,因此若是能夠本身手動模擬,那就太好了。

關鍵因素: relatedTarget 要想手動模擬mouseenter事件,須要對mouseover事件觸發時的事件對象event屬性relatedTarget瞭解。

  1. relatedTarget事件屬性返回與事件的目標節點相關的節點。

  2. 對於mouseover事件來講,該屬性是鼠標指針移到目標節點上時所離開的那個節點。

  3. 對於mouseout事件來講,該屬性是離開目標時,鼠標指針進入的節點。

  4. 對於其餘類型的事件來講,這個屬性沒有用。

從新回顧一下文章最初的那張圖,根據上面的解釋,對於ul上添加的mouseover事件來講,relatedTarget只多是

  1. ul的父元素wrap(移入ul時,此時也是觸發mouseenter事件的時候, 其實不必定,後面會說明),

  2. 或者ul元素自己(在其子元素上移出時),

  3. 又或者是子元素自己(直接從子元素A移動到子元素B)。

relatedTarget

根據上面的描述,咱們能夠對relatedTarget的值進行判斷:若是值不是目標元素,也不是目標元素的子元素,就說明鼠標已移入目標元素而不是在元素內部移動。

條件1: 不是目標元素很好判斷e.relatedTarget !== target(目標元素)

條件2:不是目標元素的子元素,這個應該怎麼判斷呢?

ele.contains

這裏須要介紹一個新的api [node.contains(otherNode)

](https://developer.mozilla.org... 表示傳入的節點是否爲該節點的後代節點, 若是 otherNode 是 node 的後代節點或是 node 節點自己.則返回true , 不然返回 false

用法案例

<ul class="list">
  <li class="item">1</li>
  <li>2</li>
</ul>
<div class="test"></div>
let $list = document.querySelector('.list')
let $item = document.querySelector('.item')
let $test = document.querySelector('.test')

$list.contains($item) // true
$list.contains($test) // false
$list.contains($list) // true

那麼利用contains這個api咱們即可以很方便的驗證條件2,接下來咱們封裝一個contains(parent, node)函數,專門用來判斷node是否是parent的子節點

let contains = function (parent, node) {
  return parent !== node && parent.contains(node)
}

用咱們封裝事後的contains函數再去試試上面的例子

contains($list, $item) // true
contains($list, $test) // false
contains($list, $list) // false (主要區別在這裏)

這個方法很方便地幫助咱們解決了模擬mouseenter事件中的條件2,可是悲催的ode.contains(otherNode),具備瀏覽器兼容性,在一些低級瀏覽器中是不支持的,爲了作到兼容咱們再來改寫一下contains方法

let contains = docEle.contains ? function (parent, node) {
  return parent !== node && parent.contains(node)
} : function (parent, node) {
  let result = parent !== node

  if (!result) { // 排除parent與node傳入相同的節點
    return result
  }

  if (result) {
    while (node && (node = node.parentNode)) {
      if (parent === node) {
        return true
      }
    }
  }

  return false
}

說了這麼多,咱們來看看用mouseover事件模擬mouseenter的最終代碼

// callback表示若是執行mouseenter事件時傳入的回調函數

let emulateEnterOrLeave = function (callback) {
  return function (e) {
    let relatedTarget = e.relatedTarget
    if (relatedTarget !== this && !contains(this, relatedTarget)) {
      callback.apply(this, arguments)
    }
  }
}

模擬mouseenter與原生mouseenter事件效果對比

html

<div class="wrap">
  wrap, mouseenter
  <ul class="mouseenter list">
    count: <span class="count"></span>
    <li>1</li>
    <li>2</li>
    <li>3</li>
  </ul>
</div>

<div class="wrap">
  wrap, emulate mouseenter,用mouseover模擬實現mouseenter
  <ul class="emulate-mouseenter list">
    count: <span class="count"></span>
    <li>1</li>
    <li>2</li>
    <li>3</li>
  </ul>
</div>

css

.wrap{
  width: 50%;
  box-sizing: border-box;
  float: left;
}

.wrap, .list{
  border: solid 1px green;
  padding: 30px;
  margin: 30px 0;
}

.list{
  border: solid 1px red;
}

.list li{
  border: solid 1px blue;
  padding: 10px;
  margin: 10px;
}

.count{
  color: red;
}

javascript

let $mouseenter = document.querySelector('.mouseenter')
let $emulateMouseenter = document.querySelector('.emulate-mouseenter')
let $enterCount = document.querySelector('.mouseenter .count')
let $emulateMouseenterCounter = document.querySelector('.emulate-mouseenter .count')

let addCount = function (ele, start) {
  return function () {
    ele.innerHTML = ++start
  }
}

let docEle = document.documentElement
  let contains = docEle.contains ? function (parent, node) {
    return parent !== node && parent.contains(node)
  } : function (parent, node) {
  let result = parent !== node

  if (!result) {
    return result
  }

  if (result) {
    while (node && (node = node.parentNode)) {
      if (parent === node) {
        return true
      }
    }
  }

  return false
}

let emulateMouseenterCallback = addCount($emulateMouseenterCounter, 0)
  
let emulateEnterOrLeave = function (callback) {
  return function (e) {
    let relatedTarget = e.relatedTarget
    if (relatedTarget !== this && !contains(this, relatedTarget)) {
      callback.apply(this, arguments)
    }
  }
}

$mouseenter.addEventListener('mouseenter', addCount($enterCount, 0), false)
$emulateMouseenter.addEventListener('mouseover', emulateEnterOrLeave(emulateMouseenterCallback), false)

效果預覽

詳細代碼點擊

代碼示例點擊

好了,咱們已經經過mouseove事件完整的模擬了mouseenter事件,可是反過頭來看看

對於ul上添加的mouseover事件來講,relatedTarget只多是

  1. ul的父元素wrap(移入ul時,此時也是觸發mouseenter事件的時候, 其實不必定,後面會說明),

  2. 或者ul元素自己(在其子元素上移出時),

  3. 又或者是子元素自己(直接從子元素A移動到子元素B)。

咱們經過排查2和3,最後只留下1,也就是mouseenter與mouseover事件一塊兒觸發的時機。既然這樣咱們爲何不像這樣判斷呢?

target.addEventListener('mouseover', function (e) {
  if (e.relatedTarget === this.parentNode) {
    // 執行mouseenter的回調要作的事情  
  }
}, false)

這樣不是更加簡單嗎?,何須要折騰經過排查2和3來作?

緣由是,target的父元素有必定的佔位空間的時後,咱們這樣寫是沒有太大問題的,可是反之,這個時候e.relatedTarget就多是target元素的父元素,又祖先元素中的某一個。咱們沒法準確判斷e.relatedTarget究竟是哪一個元素。因此經過排除2和3應該是個更好的選擇。

用mouseout模擬mouseleave事件

當mouseout被激活時,relatedTarget表示鼠標離開目標元素時,進入了哪一個元素,咱們一樣能夠對relatedTarget的值進行判斷:若是值不是目標元素,也不是目標元素的子元素,就說明鼠標已移出目標元素

咱們一樣能夠用上面封裝的函數完成

// callback表示若是執行mouseenter事件時傳入的回調函數

let emulateEnterOrLeave = function (callback) {
  return function (e) {
    let relatedTarget = e.relatedTarget
    if (relatedTarget !== this && !contains(this, relatedTarget)) {
      callback.apply(this, arguments)
    }
  }
}

詳細代碼點擊

代碼示例點擊

結尾

文中也許有些觀點不夠嚴謹,歡迎你們拍磚。

原文地址

項目地址

相關文章
相關標籤/搜索