深入浅出js事件
一.事件流
事件冒泡和事件捕获分别由微软和网景公司提出,这两个概念是为了解决页面中事件流(事件发生顺序)的问题。
<div id="outer"> <p id="inner">Click me!p> div>
上面的代码当中一个div元素当中有一个p子元素,如果两个元素都有一个click的处理函数,那么我们怎么才能知道哪一个函数会首先被触发呢?
为了解决这个问题微软和网景提出了两种几乎完全相反的概念。
1.事件冒泡
微软提出了名为事件冒泡的事件流。事件冒泡可以形象地比喻为把一颗石头投入水中,泡泡会一直从水底冒出水面。也就是说,事件会从最内层的元素开始发生,一直向上传播,直到document对象。
因此上面的例子在事件冒泡的概念下发生click事件的顺序应该是p -> div -> body -> html -> document
2.事件捕获
网景提出另一种事件流名为事件捕获与事件冒泡相反,事件会从最外层开始发生,直到最具体的元素。
上面的例子在事件捕获的概念下发生click事件的顺序应该是document -> html -> body -> div -> p
3.W3C事件阶段(event phase):
当一个DOM事件被触发的时候,他并不是只在它的起源对象上触发一次,而是会经历三个不同的阶段。简而言之:事件一开始从文档的根节点流向目标对象(捕获阶段),然后在目标对向上被触发(目标阶段),之后再回溯到文档的根节点(冒泡阶段)如图所示(图片来自W3C):
事件捕获阶段(Capture Phase)
事件从文档的根节点出发,随着DOM树的结构向事件的目标节点流去。途中经过各个层次的DOM节点,并在各节点上触发捕获事件,直到到达时间的目标节点。捕获阶段的主要任务是简历传播路径,在冒泡阶段,时间会通过这个路径回溯到文档根节点。
例如,通过下面的这个函数来给节点设置监听,可以通过将;设置成true来为事件的捕获阶段添加监听回调函数。
element.removeEventListener(<event-name>, <callback>, <use-capture>);
而,在实际应用中,我们并没有太多使用捕获阶段监听的用例,但是通过在捕获阶段对事件的处理,我们可以阻止类似click事件在某个特定元素上被触发。如下:
var form=document.querySeletor('form'); form.addEventListener('click',function(e){ e.stopPropagation(); },true);
如果你对这种用法不是很了解的话,建议设置为false或者undefined,从而在冒泡阶段对事件进行监听,这也是常用的方法。
目标阶段(Target Phase)
当事件到达目标节点时,事件就进入了目标阶段。事件在目标节点上被触发,然后逆向回流,知道传播到最外层的文档节点。
对于多层嵌套的节点,鼠标和指针事件经常会被定位到最里层的元素上。假设,你在一个div元素上设置了click的监听函数,而用户点击在了这个div元素内部的p元素上,那么p元素就是这个时间的目标元素。事件冒泡让我们可以在这个div或者更上层的元素上监听click事件,并且时间传播过程中触发回调函数。
冒泡阶段(Bubble Phase)
事件在目标事件上触发后,并不在这个元素上终止。它会随着DOM树一层层向上冒泡,直到到达最外层的根节点,一直向上传播,直到document对象。也就是说,同一事件会一次在目标节点的父节点,父节点的父节点...直到最外层的节点上触发。
绝大多数事件是会冒泡的,但并非所有的。
See the Pen dojdVG by jingwhale ( @jingwhale) on CodePen.二.事件代理(event delegation)
在JavaScript中,经常会碰到要监听列表中多项li的情形,假设我们有一个列表如下:
<ul id="list"> <li id="item1">item1li> <li id="item2">item2li> <li id="item3">item3li> <li id="item4">item4li> ul>
如果我们要实现以下功能:当鼠标点击某一li时,alert输出该li的内容,我们通常的写法是这样的:
-当列表项比较少时,直接在html里给每个li添加onclick事件
-列表项比较多时,在onload时就给每个列表项调用监听
第一种方法比较简单直接,但是没有顾及到html与JavaScript的分离,不建议使用,第二种方法的代码如下:
window.οnlοad=function(){ var ulNode=document.getElementById("list"); var liNodes=ulNode.childNodes||ulNode.children; for(var i=0;i){ liNodes[i].addEventListener('click',function(e){ alert(e.target.innerHTML); },false); } }
由上可以看出来,假如不停的删除或添加li,则function()也要不停的更改操作,易出错,因此推荐使用事件代理。
在传统的事件处理中,你按照需要为每一个元素添加或者是删除事件处理器。然而,事件处理器将有可能导致内存泄露或者是性能下降——你用得越多这种风险就越大。JavaScript事件代理则是一种简单的技巧,通过它你可以把事件处理器添加到一个父级元素上,这样就避免了把事件处理器添加到多个子级元素上。
事件代理机制
事件代理用到了两个在JavaSciprt事件中两个特性:事件冒泡以及目标元素。使用事件代理,我们可以把事件处理器添加到一个元素上,等待一个事件从它的子级元素里冒泡上来,并且可以得知这个事件是从哪个元素开始的。
事件代理实现
例如,有一个table元素,ID是“report”,我们为这个表格添加一个事件处理器以调用editCell函数。
第一步,找到目标元素
editCell函数需要判断传到table来的事件的目标元素。考虑到我们要写的几个函数中都有可能用到这一功能,所以我们把它单独放到一个名为getEventTarget的函数中:
function getEventTarget(e) { e = e || window.event;//事件对象 return e.target || e.srcElement; }
在IE里目标元素放在srcElemnt属性中,而在其它浏览器里则是target属性。
第二步,判断目标元素,进行相关操作
接下来就是editCell函数了,这个函数调用到了getEventTarget函数。一旦我们得到了目标元素,剩下的事情就是看看它是否是我们所需要的那个元素了。
function editCell(e){ var target = getEventTarget(e); if(target.tagName.toLowerCase() =='td') { // DO SOMETHING WITH THE CELL } }
由上叙述,我们可以使用事件代理来实现本小结开始对每一个li的监听。第三种方法,代码如下:
window.οnlοad=function(){ e = e || window.event; target = e.target || e.srcElement; var ulNode=document.getElementById("list"); ulNode.addEventListener('click',function(e){ if(target&&target.nodeName.toUpperCase()=="LI"){/*判断目标事件是否为li*/ alert(e.target.innerHTML); } },false); };
注:
-tagName 属性返回元素的标签名。在 HTML 中,tagName 属性的返回值始终是大写的。
-nodeName 属性指定节点的节点名称。如果节点是元素节点,则 nodeName 属性返回标签名。如果节点是属性节点,则 nodeName 属性返回属性的名称。对于其他节点类型,nodeName 属性返回不同节点类型的不同名称。试一试。
三.事件兼容处理
现代绑定中W3C 使用的是:addEventListener 和removeEventListener。IE 使用的是attachEvent 和detachEvent。我们知道IE 的这两个问题多多,并且伴随内存泄漏。所以,解决这些问题非常有必要。
那么我们希望解决非IE 浏览器事件绑定哪些问题呢?
1)支持同一元素的同一事件句柄可以绑定多个监听函数;
2)如果在同一元素的同一事件句柄上多次注册同一函数,那么第一次注册后的所有注册
都被忽略;
3)函数体内的this 指向的应当是正在处理事件的节点(如当前正在运行事件句柄的节
点);
4)监听函数的执行顺序应当是按照绑定的顺序执行;
5)在函数体内不用使用event = event || window.event; 来标准化Event 对象;
设计原理
1.通过使用传统事件绑定对IE 进行封装,模拟现代事件绑定(不使用attachEvent/detachEvent)。
2.把IE常用的Event对象配对到W3C中去
//跨浏览器添加事件绑定 function addEvent(obj, type, fn) { if (typeof obj.addEventListener != 'undefined') { obj.addEventListener(type, fn, false); } else { //创建一个存放事件的哈希表(散列表) if (!obj.events) obj.events = {}; //第一次执行时执行 if (!obj.events[type]) { //创建一个存放事件处理函数的数组 obj.events[type] = []; //把第一次的事件处理函数先储存到第一个位置上 if (obj['on' + type]) obj.events[type][0] = fn; } else { //同一个注册函数进行屏蔽,不添加到计数器中 if (addEvent.equal(obj.events[type], fn)) return false; } //从第二次开始我们用事件计数器来存储 obj.events[type][addEvent.ID++] = fn; //执行事件处理函数 obj['on' + type] = addEvent.exec; } } //为每个事件分配一个计数器 addEvent.ID = 1; //执行事件处理函数 addEvent.exec = function (event) { var e = event || addEvent.fixEvent(window.event); var es = this.events[e.type]; for (var i in es) { es[i].call(this, e); } }; //同一个注册函数进行屏蔽 addEvent.equal = function (es, fn) { for (var i in es) { if (es[i] == fn) return true; } return false; } //把IE常用的Event对象配对到W3C中去 addEvent.fixEvent = function (event) { event.preventDefault = addEvent.fixEvent.preventDefault; event.stopPropagation = addEvent.fixEvent.stopPropagation; event.target = event.srcElement; return event; }; //IE阻止默认行为 addEvent.fixEvent.preventDefault = function () { this.returnValue = false; }; //IE取消冒泡 addEvent.fixEvent.stopPropagation = function () { this.cancelBubble = true; }; //跨浏览器删除事件 function removeEvent(obj, type, fn) { if (typeof obj.removeEventListener != 'undefined') { obj.removeEventListener(type, fn, false); } else { if (obj.events) { for (var i in obj.events[type]) { if (obj.events[type][i] == fn) { delete obj.events[type][i]; } } } } }
addEvent的优点
-可以在所有浏览器中工作,就算是更古老无任何支持的浏览器
-this关键字可以在所有的绑定函数中使用,指向的是当前元素
-中和了所有防止浏览器默认行为和阻止事件冒泡的各种浏览器特定函数
-不管浏览器类型,事件对象总是作为第一个对象传入
addEvent的缺点
-仅工作在冒泡阶段(因为它深入使用事件绑定的传统方式)
知识说明:
添加、移除事件,代码如下:
var addEvent = document.addEventListener ? function(elem,type, listener, useCapture) { elem.addEventListener(type, listener, useCapture); }: function(elem, type, listener, useCapture) { elem.attachEvent('on' + type, listener); }; var delEvent = document.removeEventListener ? function(elem, type, listener, useCapture) { elem.removeEventListener(type, listener, useCapture) }: function(elem, type, listener, useCapture) { elem.detachEvent('on' + type, listener); };
阻止事件继续传播,代码如下:
function stopEvent (evt) { var evt = evt || window.event; if (evt.stopPropagation) { evt.stopPropagation(); } else { evt.cancelBubble = true; } }
取消默认行为,代码如下:
function stopEvent (evt) { var evt = evt || window.event; if (evt.preventDefault) { evt.preventDefault(); } else { evt.returnValue = false; } }
四.事件类型
事件类型有:UI(用户界面)事件,用户与页面上元素交互时触发 ;焦点事件:当元素获得或失去焦点时触发 ; 文本事件:当在文档中输入文本时触发;键盘事件:当用户通过键盘在页面上执行操作时触发;鼠标事件:当用户通过鼠标在页面上执行操作时触发;滚轮事件:当使用鼠标滚轮(或类似设备)时触发。它们之间是继承的关系,如下图:
1.
常用:window、image。例如设置默认的图片:
<img src="photo" alt="photo.jpg" onerror="this.src='defualt.jpg'">
2.
3.
4.
5.
6.
MouseEvent 顺序
从元素A上方移过
-mousemove-> mouseover (A) ->mouseenter (A)-> mousemove (A) ->mouseout (A) ->mouseleave (A)
点击元素
-mousedown-> [mousemove]-> mouseup->click
7.
详细使用,请参考js事件手册。如:http://www.w3school.com.cn/jsref/dom_obj_event.asp
-------------------------------------------------------------------------------------------------------------------------------------
完
转载需注明转载字样,标注原作者和原博文地址。
更多阅读:
http://blog.sina.com.cn/s/blog_5f54f0be0100cy49.html
http://www.cnblogs.com/luhangnote/archive/2012/08/16/2642657.html