JavaScript与HTML之间的交互是经过事件来实现的。事件,就是文档或浏览器窗口中发生的一些特定的交互瞬间。能够用侦听器来预订事件,以便事件发生的时候执行相应的代码。php
事件流html
事件流描述了从页面中接收事件的顺序,包括事件冒泡和事件捕获。node
事件冒泡浏览器
事件最开始时由最具体的元素(文档中嵌套层次最深的那个节点)接收,而后逐级向上传播到较为不具体的节点(文档)。app
譬若有以下嵌套的HTML页面:dom
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Event</title> </head> <body> <div> <p>点击</p> </div> </body> </html>
若是点击p元素,那么click事件首先在p元素上发生,这个元素是咱们单击的元素。而后,click事件沿着DOM树向上传播,在每一级节点上都会发生,直到传播到document对象。传播顺序以下:函数
p -> div -> body -> html -> document性能
事件捕获this
事件捕获的思想是不太具体的节点应该更早接收事件,最具体的节点应该最后接收到事件。事件捕获的用意在于在事件到达预约目标以前捕获它。spa
因为老版本浏览器不支持,所以不多有人使用事件捕获。
DOM事件流
“DOM2级事件”规定的事件流包括三个阶段:事件捕获阶段、处于目标阶段和事件冒泡阶段
document -> html -> body -> div -> p-> div -> body -> html -> document
IE8及更早版本不支持DOM事件流
事件处理程序
响应事件的函数叫作事件处理程序或事件侦听器,咱们能够经过以下方式为事件指定事件处理程序。
HTML事件处理程序
某个元素支持的每种事件均可以使用一个与相应事件处理程序同名的HTML特性来指定。这个特性的值应该是可以执行的JavaScript代码。
<input type="button" value="click me" onclick="alert('clicked')">
这样指定事件处理程序具备一些独到之处。首先,这样会建立一个封装着元素属性值的函数。这个函数中有一个局部变量event,也就是事件对象。
<!-- 输出 'click' --> <input type="button" value="click me" onclick="alert(event.type)">
经过event变量,能够直接访问事件对象,不须要本身定义或者从函数的参数列表中读取。
在这个函数内部,this指向事件的目标元素,例如:
<!-- 输出 click me--> <input type="button" value="click me" onclick="alert(this.value)">
关于这个动态建立的函数,另外一个有意思的地方是它扩展做用域的方式。在这个函数内部,能够像访问局部变量同样访问document以及该元素自己的成员。这个函数使用with想下面这样扩展做用域:
function() { with(document) { with(this) { //元素属性 } } }
这样一来,咱们就能够更简单的访问本身的属性,以下和前面的例子效果相同。
<!-- 输出 click me--> <input type="button" value="click me" onclick="alert(value)">
若是当前元素是个表单输入元素,则表用域中还会包含访问表单元素(父元素)的入口,这个函数就变成了以下所示:
function() { with(document) { with(this.form) { with(this) { //元素属性 } } } }
<!-- username中的值 --> <form action="bg.php"> <input type="text" name="username"> <input type="password" name="password"> <input type="button" value="Click Me" onclick="alert(username.value)"> </form>
使用HTML事件处理程序的缺点
时差问题:用户可能会在HTML元素一出如今页面上就触发相应的事件,可是当时事件处理程序可能不具有执行条件。譬如:
<input type="button" value="click me" onclick="clickFun();">
假设clickFun函数是在页面最底部定义的,那么在页面解析该函数以前点击都会引起错误。所以,不少HTML事件处理程序都会被封装到try-catch之中:
<input type="button" value="click me" onclick="try{clickFun();}catch(ex){}">
浏览器兼容问题:这样扩展事件处理程序的做用域链在不一样浏览器中会致使不一样的结果。不一样JavaScript引擎遵循的标识符解析规则略有差别,极可能会在访问非限定对象成员时出错。
代码耦合:HTML事件处理程序会致使HTML代码和JavaScript代码紧密耦合。若是要更改事件处理成程序须要同时修改HTML代码和JavaScript代码。
DOM0级事件处理程序
经过JavaScript指定事件处理程序的传统方式,就是将一个函数赋值给一个事件处理程序属性。这样的优点一是简单,二是浏览器兼容性好。
var btn = document.getElementById('btn'); btn.onclick = function() { alert('clicked'); }
经过DOM0级方式指定的事件处理程序被认为是元素的方法。所以,这时候的事件处理程序是在元素的做用域中运行;换句话说,程序中的this引用当前元素:
var btn = document.getElementById('btn'); btn.onclick = function() { alert(this.id); //输出 'btn' }
咱们能够在事件处理程序中经过this访问元素的任何属性和方法。以这种方式添加的事件处理程序会在事件流的冒泡阶段被处理。
也能够删除经过DOM0级方法指定的事件处理程序:
btn.onclick = null;
若是咱们使用HTML指定事件处理程序,那么onclick属性的值就是一个包含着在同名HTML特性中指定的代码的函数。
<input id="btn" type="button" value="click me" onclick="alert(123);"> <script> var btn = document.getElementById('btn'); //输出function onclick(event) { alert(123);} alert(btn.onclick); //单击按钮没反应 btn.onclick = null; </script>
DOM2级事件处理程序
“DOM2级事件”定义了两个方法,用于处理指定和删除事件处理程序的操做:addEventListener和removeEventListener。全部DOM节点中都包含这两个方法,而且都接收3个参数:要处理的事件名、做为事件处理程序的函数和一个布尔值。若是这个布尔值参数为true,表示在捕获阶段调用事件处理函数;若是是false,表示在冒泡阶段调用事件处理函数。
var btn = document.getElementById('btn'); btn.addEventListener('click',function() { alert(this.id); },false);
与DOM0级方法同样,添加的事件处理程序也是在其依附的元素的做用域中运行 ,另外,经过这种方式能够添加多个事件处理程序,添加的事件处理程序会按照添加它们的顺序出发。
var btn = document.getElementById('btn'); btn.addEventListener('click',function() { alert(this.id); },false); btn.addEventListener('click',function() { alert(this.type); },false);
问题
咱们给一个dom同时绑定两个点击事件,一个用捕获,一个用冒泡,那么事件的执行顺序是怎么样的?
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Event</title> <style> div { padding: 30px; border: 1px solid #000; } </style> </head> <body> <div id="one"> <div id="two"> <div id="three"> <div id="four">Click Me</div> </div> </div> </div> <script> window.onload = function() { one.addEventListener('click',function(){ alert('one'); },true); two.addEventListener('click',function(){ alert('two,bubble'); },false); two.addEventListener('click',function(){ alert('two,capture'); },true); three.addEventListener('click',function(){ alert('three,capture'); },true); four.addEventListener('click',function(){ alert('four'); },true); } </script> </body> </html>
点击two,执行结果:one two,bubble two,capture
点击three,执行结果:one two,capture three,capture two,bubble
分析:
绑定在被点击元素的事件是按照代码顺序发生,其余元素经过冒泡或者捕获“感知”的事件,按照W3C的标准,先发生捕获事件,后发生冒泡事件。全部事件的顺序是:其余元素捕获阶段事件 -> 本元素代码顺序事件 -> 其余元素冒泡阶段事件 。
经过addEventListener添加的事件处理程序只能用removeEventListener来移除;移除时传入的参数与添加处理程序时使用的参数相同,这也就意味着经过addEventListener添加的匿名函数没法移除。
var btn = document.getElementById('btn'); btn.addEventListener('click',function() { alert(this.id); },false); btn.addEventListener('click',function() { alert(this.type); },false); //不能移除 btn.removeEventListener('click',function() { alert(this.type); },false)
大多数状况下,都是将事件处理程序添加到事件流的冒泡阶段,这样能够最大限度地兼容各类浏览器。最好只在须要在事件到达目标以前截获它的时候将事件处理程序添加到捕获阶段。
IE9+、Firefox、Safari、Chrome、Opera支持DOM2级事件处理程序。
IE事件处理程序
IE实现了相似的两个方法:attachEvent和detachEvent。这两个方法接收两个参数:事件处理程序名称和事件处理程序函数。因为IE8及更早版本只支持事件冒泡,因此经过attachEvent添加的事件处理程序都会被添加到冒泡阶段。
var btn = document.getElementById('btn'); btn.attachEvent('onclick',function() { alert('clicked'); })
注意第一个参数是onclick而不是click。
使用attachEvent与使用DOM0级方法的主要区别在于事件处理程序的做用域,使用attachEvent时,事件处理程序会在全局做用域中运行,所以this等于window。
var btn = document.getElementById('btn'); btn.attachEvent('onclick',function() { alert(this === window); //true })
利用attachEvent也能够为一个元素添加多个事件处理程序,可是这些事件处理程序并非以添加它们的顺序执行,而是以相反的顺序被执行。
使用attachEvent添加的事件能够经过detachEvent来移除,条件是必须提供相同的参数,因此匿名函数将不能被移除。
支持IE事件处理程序的浏览器有IE和Opera,IE11开始将再也不支持attachEvent和detachEvent。
跨浏览器的事件处理程序
function addEvent(element, type, handler) { if (element.addEventListener) { //事件类型、须要执行的函数、是否捕捉(false表示冒泡) //IE9+支持addEventListener,IE8及如下不支持addEventListener element.addEventListener(type, handler, false); } else if (element.attachEvent) { //IE11以后再也不支持attachEvent //attachEvent添加的时间函数中this指向window //IE6-8只支持事件冒泡不支持事件捕获 element.attachEvent('on' + type, handler); } else { element['on' + type] = handler; } } // 移除事件 function removeEvent(element, type, handler) { if (element.removeEventListener) { element.removeEventListener(type, handler, false); } else if (element.datachEvent) { element.detachEvent('on' + type, handler); } else { element['on' + type] = null; } }
事件对象
在触发DOM上的某个事件时,会产生一个事件对象event,这个对象中包含着全部与事件有关的信息。
DOM中的事件对象
兼容DOM的浏览器会将一个event对象传入到事件处理程序中
var btn = document.getElementById('btn'); btn.onclick = function(event) { alert(event.type); } btn.addEventListener('click',function(event) { alert(event.type); },false);
<input id="btn" type="button" value="click me" onclick="alert(event.type)">
经常使用属性和方法
属性方法 类型 读/写 说明
cancelable Boolean 只读 代表是否能够取消事件的默认行为
currentTarget Element 只读 其事件处理程序当前正在处理事件的那个元素、
eventPhase Integer 只读 调用事件处理程序的阶段:1-捕获阶段,2-处于目标,3-冒泡阶段
preventDefault Function 只读 取消事件默认行为,若是cancelable是true则可使用这个方法
stopPropagation Function 只读 取消事件的进一步捕获或者冒泡,同时阻止任何事件处理程序被调用(DOM3级事件中新增)
target Element 只读 事件的目标
type String 只读 被触发的事件的类型
在事件处理程序内部,this始终等于currentTarget的值,而target则只包含事件的实际目标
若是直接将事件处理程序指定给了目标元素,则this、currentTarget和target包含相同的值。
若是须要经过一个函数处理多个事件时,可使用type属性:
var btn = document.getElementById('btn'); var handler = function(event) { switch(event.type) { case 'click': alert('click'); break; case 'mouseover': alert('mouseover'); break; case 'mouseout': alert('mouseout'); break; } } btn.onclick = handler; btn.onmouseover = handler; btn.onmouseout = handler;
事件对象的eventPhase属性表示事件当前正位于事件流的哪一个阶段,须要注意的是尽管“处于目标”发生在冒泡阶段,可是eventPhase仍然一支等于2,当eventPhase等于2时,this、target、currentTarget始终是相等的。
注意:只有在事件处理程序执行期间,event对象才会存在,一旦事件处理程序执行完成,event对象就会被销毁。
IE中的事件对象
与访问DOM中的event对象不一样,要访问IE中的event对象有几种不一样的方式,取决于指定事件处理程序的方法。在使用DOM0级方法添加事件处理程序时,event对象做为window对象的一个属性存在。
var btn = document.getElementById('btn'); btn.onclick = function() { var event = window.event; alert(event.type); }
IE9+中event对象也会做为参数被传入到事件处理程序中,可是IE9和IE10中参数event和window.event并非同一个对象,而IE11中参数event和window.event为同一个对象。
var btn = document.getElementById('btn'); btn.onclick = function(event) { var event1 = window.event; alert(event === event1); //IE11中为true }
若是事件处理程序是使用attachEvent添加的,那么就会有一个event对象传入事件处理函数中,同时咱们也能够经过window对象来访问event对象,可是它们是不相等的。
经常使用属性和方法
属性方法 类型 读/写 说明
cancelBubble Boolean 读/写 默认值为false,将其设置为true能够消除事件冒泡
returnValue Element 读/写 默认值为true,将其设置为false能够取消事件的默认行为
srcElement Element 只读 事件的目标(至关于DOM中target属性)
type String 只读 被触发的事件的类型
由于使用attachEvent添加的事件处理程序中this指向window,因此咱们一般使用srcElement来代替this。
跨浏览器的事件对象
var EventUtil = { // 阻止事件 (主要是事件冒泡,由于IE不支持事件捕获) stopPropagation : function(ev) { if (ev.stopPropagation) { ev.stopPropagation(); } else { ev.cancelBubble = true; } }, // 取消事件的默认行为 preventDefault : function(event) { if (event.preventDefault) { event.preventDefault(); } else { event.returnValue = false; } }, // 获取事件目标 getTarget : function(event) { return event.target || event.srcElement; }, // 获取event对象的引用 getEvent : function(event) { return event ? event : window.event; } }
事件代理
由于事件有冒泡机制,全部子节点的事件都会顺着父级节点跑回去,因此咱们能够经过监听父级节点来实现监听子节点的功能,这就是事件代理。
使用事件代理主要有两个优点:
addEvent(ul2, 'click', handler) function addEvent(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; } } function handler(ev) { var ev = ev || event; var target = ev.target || ev.srcElement; //找到a元素 if (target.nodeName.toLowerCase() == 'a') { //a添加的事件 } }
jQuery的写法:
$('#ul1 a').on('click', function(){ alert('正在监听'); }); //改成 $('#ul2').on('click', 'a', function(){ alert('正在监听'); });
总结:
1. addEventListener()和attachEvent()的区别
2. 解决attchEvent事件处理函数中 this指向window的方法
1) 使用事件处理函数.apply(事件对象,arguments)
这种方式的缺点是绑定的事件没法取消绑定,缘由上面已经说了,匿名函数和匿名函数之间是互不相等的。
var object=document.getElementById('xc'); function handler(){ alert(this.innerHTML); } object.attachEvent('onclick',function(){ handler.call(object,arguments); });
2) 使用事件源代替this关键字
如下代码仅适用于IE6 IE7 IE8,这种方式彻底忽略this关键字,但写起来稍显麻烦。
function handler(e){ e = e||window.event; var _this = e.srcElement||e.target; alert(_this.innerHTML); } var object = document.getElementById('xc'); object.attachEvent('onclick',handler);
/* * 添加事件处理程序 * @param object object 要添加事件处理程序的元素 * @param string type 事件名称,如click * @param function handler 事件处理程序,能够直接以匿名函数的形式给定,或者给一个已经定义的函数名。 * @param boolean remove 是不是移除的事件,本参数是为简化下面的removeEvent函数而写的,对添加事件处理程序不起任何做用 */ function addEvent(object,type,handler,remove){ if(typeof object != 'object' || typeof handler != 'function') return; try{ object[remove ? 'removeEventListener' : 'addEventListener'](type,handler,false); } catch( e ){ var i, l, xc = '_' + type; object[xc] = object[xc] || []; if(remove){ l = object[xc].length; for(i = 0;i < l;i++){ if(object[xc][i].toString() === handler.toString()){ object[xc].splice(i,1); } } } else{ l = object[xc].length; var exists = false; for(i = 0;i < l;i++){ if(object[xc][i].toString() === handler.toString()) { exists = true; } } if(!exists) object[xc].push(handler); } object['on' + type] = function(){ l = object[xc].length; for(i = 0;i < l;i++){ object[xc][i].apply(object,arguments); } } } } /* * 移除事件处理程序 */ function removeEvent(object,type,handler){ addEvent(object,type,handler,true); }