onmouseover & onmouseout

问题描述

在很多情况下,我们需要这样的一种效果,当鼠标进入元素时做某些操作,而当鼠标离开元素时又做另外一些操作。很自然的我们会想到DOM事件中的onmouseover和onmouseout事件,其中onmouseover表示的是当鼠标从元素之外移入元素时触发的事件,而onmouseout则表示鼠标从元素移出时触发的事件。具体效果请看这个页面

从页面的效果中可以看出以下两个问题:

  1. 当鼠标从当前元素进入到其子元素时会触发当前元素的onmouseout事件,也就是说视其离开了当前元素。
  2. 当子元素中发生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.

3 Comments »

 
 

Leave a Reply

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped="">