你真的了解DOM事件么?
你真的了解DOM事件么?
我们大家都知道,人与人之间的交流可以通过语言,文字,肢体动作,面部微表情等,但是你知道Javascript和HTML之间是通过什么进行交互的么?你又知道Javascript和HTML之间是如何进行交互的么?如果你不是那么清楚,可以看这篇文章。。。
前面的话:
这篇博文由浅入深,先介绍两种事件流,然后介绍常见的事件处理程序以及它们之间的差异(涉及到一个简单的兼容的处理函数),后面还会针对事件对象进行深入学习,最后则是通过DOM事件实现的小案例。
事件流:
关于事件流,简单的来说就是:事件流描述的是从页面中接受事件的顺序。下面是《Javascript高级程序设计》里面的说法:
当浏览器发展到第四代时(IE4和Netscape Communicator 4),浏览器团队遇到一个很有意思的问题:页面的哪一部分会拥有特定的事件?想象下在一张纸上有一组同心圆,如果你把手指放在圆心上,那么你的手指指向的不是一个圆,而是一组圆。两家公司的开发团队在看待浏览器事件方面还是一致的。如果你单击了某个按钮,那么同时你也单击了按钮的容器元素,甚至整个页面。
事件流描述的是从页面中接受事件的顺序。但有意思的是,IE和Netscape开发团队居然提出了两个截然相反的事件流概念。IE的事件流是事件冒泡流,而Netscape的事件流是事件捕获流。
事件冒泡
IE的事件流叫做事件冒泡,即事件开始时由最具体的元素接受,然后逐级向上传播到较为不具体的节点。看下面的例子
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>事件流</title> </head> <body> <input type="button" id="btn" value="" /> </body> </html>
点击按钮,那么这个click事件会按照这样传播:
<input-->body-->html-->document
事件捕获
Netscape团队提出的另一种事件流叫做事件捕获。事件捕获的思想是不太具体的DOM节点应该更早接收到事件,而最具体的节点应该最后接收到事件。
针对上面同样的例子,点击按钮,那么此时click事件会按照这样传播:
document-->html-->body-->input
虽然事件捕获是Netscape唯一支持的事件流模型,但IE9、Safari、Chrome、Opera和Firefox目前也都支持这种事件流模型。但由于老版本的浏览器不支持,因此很少有人使用事件捕获。
事件处理程序
HTML事件处理程序
HTML事件处理程序(现在不建议使用了):事件直接加在HTML代码中,例如:
<input type="button" id="btn" value="HTML事件处理程序" onclick="alert('HTML事件处理程序')" />
或者:
<input type="button" id="btn1" value="HTML事件处理程序" onclick="demo1()" /> function demo1(){ alert("HTML事件处理程序"); }
缺点:HTML和js代码高耦合,如果修改,就要修改两个地方--HTML元素内和script函数。
DOM0级事件处理程序
DOM0级事件处理程序,是一种较传统的方式:把一个函数赋值给一个事件的处理程序属性。例如:
var btn2 = document.getElementById("btn2"); btn2.onclick = function(){ alert("DOM0级事件处理程序"); }
现在用得比较多,优势是简单(只需要考虑JS不考虑HTML)且跨浏览器。如果想取消点击事件,将其设为null即可。
var btn2.onclick = null;//这时点击不会弹出窗口,表示已经取消了该点击事件
下面是我学习时候的一些思考:
思考一:DOM0级事件是否可以同时添加多个不同事件处理方法
var btn2 = document.getElementById("btn2"); btn2.onclick = function(){ console.log("DOM0级事件处理程序-click-01"); } btn2.onmouseover = function(){ console.log("DOM0级事件处理程序-mouseover"); }
结果:鼠标经过,控制台出现:DOM0级事件处理程序-mouseover;鼠标点击,控制台出现:DOM0级事件处理程序-click-01。
说明:DOM0级事件可以同时添加多个不同事件处理方法
思考二:DOM0级事件是否可以同时添加多个相同事件处理方法
我们在前面的基础上添加如下代码:
btn2.onclick = function(){ console.log("DOM0级事件处理程序-click-02"); }
结果:鼠标经过,控制台出现:DOM0级事件处理程序-mouseover;鼠标点击,控制台出现:DOM0级事件处理程序-click-02。(在这里你可能以为会出现DOM0级事件处理程序-click-01和DOM0级事件处理程序-click-02)
说明:DOM0级事件不可以同时添加多个相同事件处理方法(这样说好像不太严格,大概意思就是直接忽略了前面的onclick,只显示最后的onclick的结果。
那么这里就设下一个疑问:我们应该如何让DOM同时绑定多个相同事件呢???我们接着往下看
DOM2级事件处理程序
DOM2级事件处理程序定义了两个方法--用于处理指定和删除事件处理程序的操作:
addEventListener()添加事件监听程序
removeEventListener()移除事件监听程序
三个参数设置;事件名称,处理方法(函数),布尔值--false:表示在冒泡阶段调用事件处理程序(可以最大限度的兼容浏览器),一般设为false;true表示在捕获阶段调用事件处理程序
来看下面的例子:
var btn3 = document.getElementById("btn3"); btn3.addEventListener('click',function(){ console.log("DOM2级事件处理程序-click-01"); },false) //DOM2级事件处理程序-click-01
与DOM0级不同的是,我们移除需要使用removeEventListener(),代码如下:
var btn3 = document.getElementById("btn3"); btn3.addEventListener('click',function(){ console.log("DOM2级事件处理程序-click-01"); },false) btn3.removeEventListener('click',function(){ console.log("DOM2级事件处理程序-click-移除成功"); },false) //DOM2级事件处理程序-click-01
在这里,你很有可能和我想的一样,DOM2级事件处理程序已经移除完了。但是结果是DOM2级事件处理程序-click-01依然还在,按照理论上来说不应该是被移除了么?但是为什么还在?
原因:通过addEventListener方法添加的事件处理程序只能用removeEventListener来移除,移除时传入的参数与添加处理程序时使用的参数相同。这就意味着通过addEventListener添加的匿名函数将无法移除。真的是这样么?我们继续来看例子:
var btn3 = document.getElementById("btn3"); var handler = function demo3(){ alert("DOM2级事件处理程序"); } btn3.addEventListener('click',handler,false) //在不加后面的代码的时候弹框显示DOM2级事件处理程序 btn3.removeEventListener('click',handler,false) //加了removeEventListener后点击,没有弹框,说明该点击事件已经被移除了
接下来我们同样思考几个问题:
思考一:DOM2级事件是否可以同时添加多个不同事件处理方法
var btn3 = document.getElementById("btn3"); btn3.addEventListener('click',function(){ console.log("DOM2级事件处理程序-click-01"); },false) btn3.addEventListener('mouseover',function(){ console.log("DOM2级事件处理程序-mouseover"); },false)
结果:鼠标经过,控制台出现:DOM2级事件处理程序-mouseover;鼠标点击,控制台出现:DOM2级事件处理程序-click-01。
说明:DOM2级事件可以同时添加多个不同事件处理方法
思考二:DOM2级事件是否可以同时添加多个相同事件处理方法
var btn3 = document.getElementById("btn3"); btn3.addEventListener('click',function(){ console.log("DOM2级事件处理程序-click-01"); },false) btn3.addEventListener('mouseover',function(){ console.log("DOM2级事件处理程序-mouseover"); },false) btn3.addEventListener('click',function(){ console.log(this.id); },false)
结果:鼠标经过,控制台出现:DOM2级事件处理程序-mouseover;鼠标点击,控制台出现:DOM2级事件处理程序-click-01和btn3。
说明:DOM2级事件可以同时添加多个不同事件处理方法,同时在这里可以用this引用被触发的元素。
注意:
1、要处理的事件名(事件名去掉“on”,如点击事件需写成click);
2、作为事件处理程序的函数名(函数不需要带括号,只要函数名或者直接写函数体也可以);
3、布尔值(true表示使用事件捕获流,false表示使用事件冒泡流,一般使用false兼容各种浏览器)。
4、通过addEventListener添加的事件只能通过removeEventListener来移除;DOM 0级和DOM 2级事件可以在一个DOM上添加多个事件,按顺序执行
5、DOM2级事件处理程序适用于:IE9、Firefox、Safari、Chrome和Opera
思考:那么如果我们想要兼容低版本的IE该怎么办呢?请继续往下阅读
IE事件处理程序
(声明:IE事件处理程序的代码没有进行测试)
IE实现了与DOM中类似的两个方法:attachEvent()和detachEvent(),这两个方法接受相同的两个参数:事件处理程序名称与事件处理函数程序函数(因为IE8及更早版本只支持时间冒泡,所以没有第三个参数)。下面我们来看代码:
var btn4 = document.getElementById("btn4"); btn4.attachEvent('onclick',function(){ console.log("IE事件处理程序-onclick-01"); });
这里需要注意的是,第一个参数是onclick,而不是click。另外与DOM0级中的不同在于作用域,看下面的代码:
var btn4 = document.getElementById("btn4"); btn4.attachEvent('onclick',function(){ console.log(this === window); //true });
其他需要注意的有:
1、attachEvent方法也可以为一个元素添加一个或多个事件处理程序,当添加多个相同的事件处理程序的时候,这些事件处理程序不是以添加它们的顺序而执行,而是以相反的顺序被触发。比如下面这个例子:
var btn4 = document.getElementById("btn4"); btn4.attachEvent('onclick',function(){ console.log("hello"); }); btn4.attachEvent('onclick',function(){ console.log("World"); });
这段代码用DOM0级的方法得到的结果为World,用DOM2级的方法得到结果为先是hello然后是World,而在这里的结果为先是World然后是hello。
2、detachEvent()方法和DOM2级中移除的方法一样,同样也需要注意中间添加的匿名函数不能被移除,这里就不再举例说明了。
从HTML事件处理程序到DOM0级事件处理程序,再到DOM2级处理程序,还有IE事件处理程序,我们在使用的时候为了让用户体验最佳,我们该让我们的代码兼容各种浏览器,那么我们有什么好的方法让我们的代码兼容到各种浏览器中呢?
跨浏览器的事件处理程序
为了解决跨浏览器的问题,我们不得不集中的把前面几种方法柔和在一起,然后就出现了跨浏览器的事件处理程序。(我的思路如下)
1、先考虑添加事件的情况,在添加的时候,我们要知道给谁添加?添加的是什么事件?然后事件触发的是什么函数?
给谁添加?当然是我们的元素了,因为还不知道,这里我们先设置为element
添加的是什么事件?click?onclick?onmouseover?我们也还是不知道,先设置为type吧
添加什么函数?前面的例子中有用到过handler这里也先就用这个
function(element, type, handler) {……}
2、我们要判断我们的浏览器是否支持相关的操作,这里我们可以用if-else来判断
if(element.addEventListener) { //DOM2级事件处理程序 } else if(element.attachEvent) { //IE事件处理程序 } else { //DOM0级事件处理程序 }
3、针对DOM2级事件处理程序、IE事件处理程序和DOM0级事件处理程序它们有相似的地方,然后我们通过前面可以知道,它们对于事件的处理有点不和谐,我们该怎样呢?很明显,添加"on"比去掉"on"更简单,所以比如点击事件的type应该为click,然后在需要用onclick的地方,使用'on'+type,然后各自保留各自的特点
DOM2级事件处理程序:element.addEventListener(type, handler, false);
IE事件处理程序:element.attachEvent('on' + type, handler);
DOM0级事件处理程序:element['on' + type] = handler;
4、到这里基本的添加情况以完成,接下来用同样的方法来考虑移除事件的情况(步骤省略)
5、然后分别给添加事件函数和移除事件函数进行命名addHandler和deleteHandler,并封装起来,得到我们的最终效果:
var eventUtil = { addHandler: function(element, type, handler) { if(element.addEventListener) { element.addEventListener(type, handler, false); } else if(element.attachEvent) { element.attachEvent('on' + type, handler); } else { element['on' + type] = handler; } }, deleteHandler: function(element, type, handler) { if(element.removeEventListener) { element.removeEventListener(type, handler, false); } else if(element.detachEvent) { element.detachEvent('on' + type, handler); } else { element['on' + type] = null; } } }
事件对象
DOM中的事件对象
前面我们已经封装好一个函数了,我们后面的操作直接引用前面的那个函数。说到事件对象,我们先不得不说事件对象的两个重要属性type和target
其中type是获取事件的类型,而target是获取事件的目标。直接看代码:
var btn = document.getElementById("btn"); eventUtil.addHandler(btn,'click',showMes); function showMes(event){ alert(event.type);//结果是弹出click }
结果是弹出click,说明了该事件的类型是点击事件
var btn = document.getElementById("btn"); eventUtil.addHandler(btn,'click',showMes); function showMes(event){ alert(event.target);//结果是弹出[object HTMLInputElement] }
结果是弹出[object HTMLInputElement],说明该事件的目标是input元素
下面我们运行一段代码,来看一个有趣的现象:
<div id="box"> <input type="button" id="btn" value="DOM中的事件对象" /> <a href="www://baidu.com" id="go">百度</a> </div> function showMes(event){ alert(event.target.nodeName); } function showBox(){ alert("这是放盒子的box") } var btn = document.getElementById("btn"); var box = document.getElementById("box"); eventUtil.addHandler(btn,'click',showMes); eventUtil.addHandler(box,'click',showBox);
运行结果:鼠标点击按钮,分别弹出了“INPUT”和“这是放盒子的box”,我们只点击了按钮,为什么按钮所在的div也会显示呢?查资料知道这是因为事件冒泡机制导致的。
那么我们该如何阻止事件冒泡呢?
我们的事件对象给我们提供了一种方法:通过stopPropagation()方法来实现,代码示例如下:
function showMes(event){ alert(event.target.nodeName); event.stopPropagation();//阻止事件冒泡 } function showBox(){ alert("这是放盒子的box") } var btn = document.getElementById("btn"); var box = document.getElementById("box"); eventUtil.addHandler(btn,'click',showMes); eventUtil.addHandler(box,'click',showBox);
光知道阻止事件冒泡行为还不行我们还需要知道的一种方法:阻止事件默认行为-->preventDefault()方法
比如我们知道的a标签有一个默认行为就是跳转到目标地址,如果我们想要阻止这种行为我们就可以使用该方法,代码如下:
<a href="www://baidu.com" id="go">百度</a> var go = document.getElementById("go"); function showGo(event){ event.stopPropagation();//阻止事件冒泡 event.preventDefault();//阻止事件的默认行为 } eventUtil.addHandler(go,'click',showGo);
运行结果是,无论怎样点击,都跳转不到百度页面。
在a标签里面阻止事件默认行为还有一种方法就是直接在a标签里面添加,比如<a href="javascript:;">aa</a>
补充:在需要通过一个函数处理多个事件时,可以使用type属性,比如:
var btn = document.getElementById("btn"); var handler = function(event){ switch(event.type){ case"click": alert("Clicked"); break; case"mouseover": event.target.style.backgroundColor = "red"; break; case"mouseout": event.target.style.backgroundColor = ""; break; } }; btn.onclick = handler; btn.onmouseover = handler; btn.onmouseout = handler;
IE中的事件对象
【type属性】用于获取事件类型
【srcElement属性】用于获取事件的目标(DOM中用的是target),所以,可以使用
event=event || window.event;(考虑IE8以前的版本) var ele=event.target event.srcElement;(考虑非IE与IE)
【cancelBubble属性】用于阻止事件冒泡,设为true表示阻止冒泡,false表示不阻止冒泡。
【returnValue属性】用于阻止事件的默认行为,设为false表示阻止。
这里只介绍IE事件对象的常用属性,关于它的具体例子就不做介绍了
跨浏览器的事件对象:
搞清楚前面的问题,下面的一段代码看起来就简单了。它直接将前面提到的所有方法和属性一起封装起来,并保存到一个单独的js文件中,命名为event.js,其代码如下:
var eventUtil = { // 添加句柄 addHandler: function(element, type, handler) { if(element.addEventListener) { element.addEventListener(type, handler, false); } else if(element.attachEvent) { element.attachEvent('on' + type, handler); } else { element['on' + type] = handler; } }, // 删除句柄 removeHandler: function(element, type, handler) { if(element.removeEventListener) { element.removeEventListener(type, handler, false); } else if(element.detachEvent) { element.detachEvent('on' + type, handler); } else { element['on' + type] = null; } }, //获取事件 getEvent: function(event) { return event ? event : window.event; }, //获取事件类型 getType: function(event) { return event.type; }, //获取元素 getElement: function(event) { return event.target || event.srcElement; }, //阻止事件默认行为 preventDefault: function(event) { if(event.preventDefault) { event.preventDefault(); } else { event.returnValue = false; } }, //阻止事件冒泡 stopPropagation: function(event) { if(event.stopPropagation) { event.stopPropagation(); } else { event.cancelBubble = true; } } }
我们可以用如下代码来测试点击a事件
window.onload=function(){ var go=document.getElementById('go'), box=document.getElementById('box'); eventUtil.addHandler(box,'click',function(){ alert('我是整个父盒子'); }); eventUtil.addHandler(go,'click',function(e){ //e=eventUtil.getEvent(e); e=e || window.event; alert(eventUtil.getElement(e).nodeName); eventUtil.preventDefault(e); eventUtil.stopPropagation(e); }); }
事件类型
因为事件类型比较多,这里主要通过两个具体的案例来展示,不做详细介绍
案例一:用鼠标或键盘事件来控制抽奖。
效果截图:
查看地址:http://sandbox.runjs.cn/show/0pibyaes
案例二:模仿扣扣登陆时的拖曳效果(主要学习其中如何实现拖曳效果的思维)
效果截图:
查看地址:http://sandbox.runjs.cn/show/7mhb7pfw
更多参考资料:
参考书籍:《Javascript高级程序设计(第三版)》
参考博客: https://my.oschina.net/u/1403185/blog/184960