onmouseover & onmouseout
问题描述
在很多情况下,我们需要这样的一种效果,当鼠标进入元素时做某些操作,而当鼠标离开元素时又做另外一些操作。很自然的我们会想到DOM事件中的onmouseover和onmouseout事件,其中onmouseover表示的是当鼠标从元素之外移入元素时触发的事件,而onmouseout则表示鼠标从元素移出时触发的事件。具体效果请看这个页面。
从页面的效果中可以看出以下两个问题:
- 当鼠标从当前元素进入到其子元素时会触发当前元素的onmouseout事件,也就是说视其离开了当前元素。
- 当子元素中发生onmouseove和onmouseout事件时,由于冒泡机制会被父元素捕获。
以上两个现象在实际应用中往往是多余而负面的,因为大多数需求不会把鼠标在内部移动当作mouseout,而是要求当鼠标真实的离开了元素所覆盖的区域时才真正触发;此外,也不希望鼠标在元素内部移动时反复的触发mouseout和mouseover。因此,这两个问题是需要解决的。
解决方案
为了解决这两个问题,IE非常有才的引入了另外两个事件onmouseenter和onmouseleave,这两个事件恰好符合上述需求,不信试试这里。从这个例子中你会发现只有当鼠标真实离开了红色DIV覆盖的区域时才会触发onmouseleave事件,而且这两个事件无冒泡效果。此外,这两个事件还有setCapture的效果,即使你使用Ctrl+TAB把窗口来回切换,依然可以正确的触发事件。
因此,在IE下可以通过onmouseenter和onmouseleave轻松的解决以上问题。然后在FF以及其他浏览器中并没有引入这样的事件,于是自己折腾出这个方案。
var t = document.getElementById('test'); var status = document.getElementById('status'); //判断child是否是father的子孙结点 function isChild(father,child,self){ var up = self ? child:child.parentNode; var is = false; while(up){ if( up == father ){ is = true; break; } up = up.parentNode; } return is; } //标识是否已经进入元素 var enter = false; document.onmousemove = function(e){ e = e || event; var target = e.target || e.srcElement; //如果鼠标在子孙结点则忽略 if( isChild(t,target) ) return; //第一次触发目标元素的mousemove则视为enter if( !enter && target == t){ status.innerHTML += 'enter -> '; enter = true; } else if(enter && target != t){ status.innerHTML += 'out -> '; enter = false; } }
实现的基本思路是在document上注册onmousemove事件,并通过事件的target和一系列的标志位来判断鼠标是否进入了目标元素。该方法的确达到了目的,但是总体感觉不理想。一个是代码太多了;此外,由于注册了document.onmousemove事件,因此只要鼠标在窗口内移动就会被捕获执行,所以效率是一个问题。最后,该方法目前没有使用setCapture处理鼠标移动窗口外的情况,因此使用Ctrl+TAB将鼠标移出时会出问题。如果要解决该问题还需要增加代码。
其实,仔细研读一下W3C文档关于事件方面的属性,还是可以发现更好的解决方案的。在目前现代浏览器的事件属性中都包含relatedTarget这样一个属性,具体的解释如下:
- 对于 mouseover 事件来说,该属性是鼠标指针移到目标节点上时所离开的那个节点。
- 对于 mouseout 事件来说,该属性是离开目标时,鼠标指针进入的节点。
- 对于其他类型的事件来说,这个属性没有用。
也就是说,relatedTarget始终表示的是除触发事件元素本身之外的另一方元素,以第一个例子为例,当鼠标从外界进入到红色的DIV时,红色DIV的mouseover事件中的relatedTarget表示的元素是html,而当鼠标从红色DIV进入到蓝色DIV时,红色DIV的mouseout事件的relatedTarget表示的元素是蓝色DIV。从此可以看出,无论是mouseout还是mouseover事件,只要relatedTarget所表示的元素是当前元素的子孙结点则应该忽略。具体的例子请看这里。
function isMouseLeaveOrEnter(e, el) { if (e.type != 'mouseout' && e.type != 'mouseover') return false; //在标准浏览器下使用relatedTarget,在IE下mouseout时使用toElement,mouseover使用fromElement var reltg = e.relatedTarget ? e.relatedTarget : e.type == 'mouseout' ? e.toElement:e.fromElement; //检测reltg是否是el的子孙结点 while (reltg && reltg != el) reltg = reltg.parentNode; //如果reltg遍历至空则说明不是子孙结点,返回true;如果reltg==el则表明reltg是el的子孙结点,返回false; return (reltg != el); } window.onload = function(){ var t = document.getElementById('test'); var status = document.getElementById('status'); t.onmouseover = function(e){ e = e || event; //判定是否为需要的mouseover if( isMouseLeaveOrEnter(e,t) ) status.innerHTML += 'enter -> '; } t.onmouseout = function(e){ e = e || event; //判定是否为需要的mouseout if( isMouseLeaveOrEnter(e,t) ) status.innerHTML += 'out -> '; } }
这个方案确实要简洁利索不少,而且支持焦点捕获。
You can follow any responses to this entry through the RSS 2.0 feed. You can leave a response, or trackback from your own site.
受益非浅啊,终于解决了我的难题~~
思路很好啊,这方面flash好多了,用rollover和rollout来搞
解决方案不错!!
以前遇到过这样的一个问题:在div中声明了mouseover和mouseout事件,div内部存在a元素,最后杯具就发生了……
看了这篇文章,解惑已~