1.简介
本文将从事件流(捕获阶段、目标阶段、冒泡阶段)、事件处理程序(HTML级、DOM0级、DOM2级、简单的兼容示例)、事件委托、模拟事件(DOM中的模拟事件、IE中的模拟事件、自定义事件)、事件对象、事件类型这几个方面来简要的介绍DOM事件。
2.事件流
事件流描述的是从页面中接收事件的顺序。
–《JavaScript高级程序设计》
事件冒泡
IE的事件流叫做事件冒泡(event bubbling),即事件开始时由最具体的元素(文档中嵌套层次最深的那个节点)接收,然后逐级向上传播到较为不具体的节点(文档)。
–《JavaScript高级程序设计》
事件捕获
Netscape Communicator团队提出的另一种事件流叫做事件捕获(event capturing)。事件捕获的思想是不太具体的节点应该更早的接收到事件,而最具体的节点应该最后接收到事件。
–《JavaScript高级程序设计》
DOM事件流
“DOM2级事件”规定的事件流包括三个阶段:事件捕获阶段、处于目标阶段和事件冒泡阶段。
–《JavaScript高级程序设计》
多数支持DOM事件流的浏览器都实现了一种特定的行为;即使“DOM2级事件”规范明确要求捕获阶段不会涉及事件目标,但IE9、Safari、Chrome、Firefox和Opera9.5及更高版本都会在捕获阶段触发事件对象上的事件。结果,就是有两个机会在目标对象上操作事件。
–《JavaScript高级程序设计》
3.事件处理程序
事件就是用户或者浏览器自身执行的某种动作,而响应某个事件的函数就叫做事件处理程序(事件侦听器)。
事件处理程序主要分为三种,分别是HTML事件处理程序、DOM0级事件处理程序和DOM2级事件处理程序,下面将分别对这三种事件处理程序进行总结分析。
HTML事件处理程序
示例代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>HTML事件处理程序</title>
<script type="text/javascript">
function showMessage() {
console.log('wrapper', this, event);
}
function showBodyMessage() {
console.log('wrapper', this, event);
}
var i = 10;
</script>
</head>
<body onclick="showBodyMessage" id="body">
<div onclick="showMessage()" id="wrapper">
<div onclick="console.log('inner', this, event, i)" id="inner">
click me!
</div>
</div>
</body>
</html>
分析总结:
- 添加监听函数
- 1.1 有两种方式用来添加监听函数,一是在 on- 属性后直接写代码,点击后会执行;二是在 on- 属性后写一个函数。
- 1.2 on- 属性的值是一个将要执行的代码,在将函数名添加到值的位置时,不要忘记加上圆括号。
- 移除监听函数。
- 无法直接移除监听函数。
- 是否可以添加多个事件处理函数
- 不可以添加多个事件处理函数。
- 事件传播
- 在主流浏览器上都是:事件冒泡。
- this 指向
- 直接在属性值里写代码,this指向是绑定事件的那个Element节点。
- 在值的位置写函数的情况,this指向是全局window对象。
- event对象
- 直接在属性值里写代码,可以直接使用event对象。
- 在值的位置写函数的情况,可以在不传参的情况下直接使用,也可以在参数列表中将event对象传过去。
- 其他
- 事件处理程序中的代码在执行时,有权访问全局作用域中的任何代码。
- 通过event变量,可以直接访问事件对象,而你不用自己定义,也不用从函数的参数列表中读取。
- HTML代码与JavaScript代码紧密耦合。所以一般不推荐这种写法。
DOM0级事件处理程序
示例代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<script>
window.onload = function() {
var wrapperEle = document.getElementById('wrapper');
var innerEle = document.getElementById('inner');
innerEle.onclick = function() {
console.log('inner', this, event);
}
wrapperEle.onclick = showMessage;
function showMessage() {
console.log('wrapper', this, event);
}
// 移除监听函数
// wrapperEle.onclick = null;
}
</script>
</head>
<body>
<div id="wrapper" onclick="console.log('HTML 事件处理程序')">
<div onclick="console.log('html inner click')" id="inner">
click me!
</div>
</div>
</body>
</html>
分析总结:
- 添加监听函数
- 获取对象,将这个属性的值设置为一个函数,就可以指定一个事件处理函数。
- 移除监听函数
- 在其之后,将 on- 属性的值设置为 null,即可移除事件处理程序。
- 是否可以添加多个事件处理函数
- 可以添加多个事件处理程序,但是只有最后一个会被执行。
- 如果在该节点上既有HTML事件处理程序,又有DOM0级事件处理程序,HTML事件处理程序会被覆盖,不会被执行。
- 事件传播
- 在主流浏览器上为:事件冒泡。
- this指向
- 绑定事件的Element节点。
- event对象
- 可以传过去,即在函数参数位置写上形参。
- 也可以不传形参直接使用,但在函数体内只能以event引用event对象。
- 其他
- 如果一个节点上既有HTML级事件处理程序,又有DOM 0级事件处理程序,HTML级事件处理程序会被覆盖,不会执行。
DOM2 级事件处理程序
示例代码
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>DOM2级事件</title>
<script type="text/javascript">
window.onload = function () {
// 测试捕获冒泡阶段代码
(function () {
var phases = ['', '捕获阶段', '目标阶段', '冒泡阶段'];
var rootEle = document.getElementById('root');
var wrapperEle = document.getElementById('wrapper');
var innerEle = document.getElementById('inner');
// 冒泡
rootEle.addEventListener('click', function () {
console.log('root click', '冒泡监听');
console.log(this, event, phases[event.eventPhase]);
}, false);
wrapperEle.addEventListener('click', function (e) {
console.log('wrapper click', '冒泡监听');
console.log(this, e, phases[event.eventPhase]);
}, false);
innerEle.addEventListener('click', function () {
console.log('inner click', '冒泡监听');
console.log(this, event, phases[event.eventPhase]);
}, false);
// 捕获
rootEle.addEventListener('click', function () {
console.log('root click', '捕获监听');
console.log(this, event, phases[event.eventPhase]);
}, true);
wrapperEle.addEventListener('click', function () {
console.log('wrapper click', '捕获监听');
console.log(this, event, phases[event.eventPhase]);
}, true);
innerEle.addEventListener('click', function () {
console.log('inner click', '捕获监听');
console.log(this, event, phases[event.eventPhase]);
}, true);
})();
// 测试监听、移除监听代码
(function () {
var innerEle = document.getElementById('inner');
var click1 = function () {
console.log(1);
};
var click2 = function () {
console.log(2);
};
var click3 = function () {
console.log(3);
};
innerEle.addEventListener('click', click1, false);
innerEle.addEventListener('click', click2, false);
innerEle.addEventListener('click', click3, false);
// 这种方式监听的事件是无法移除监听的。
innerEle.addEventListener('click', function () {
console.log(4);
}, false);
// innerEle.removeEventListener('click', click2, false);
// 无法移除监听
// innerEle.removeEventListener('click', function () {
// console.log(4);
// }, false);
})();
}
</script>
</head>
<body id="root">
<div id="wrapper">
<div id="inner">
click me!
</div>
</div>
</body>
</html>
分析总结:
- 添加监听函数
- 使用 addEventListener 函数
- 移除监听函数
- 使用 removeEventListener 函数
- 参数列表要求与 addEventListener 完全一致
- 是否可以添加多个事件处理函数
- 可以,按照添加顺序执行
- 事件传播
- 当指定第三个参数为 true 时,按照捕获方式调用。
- 其他情况,为冒泡方式调用。
- this指向
- 绑定事件的Element节点。
- event对象
- 可以传过去,即在函数参数位置写上形参。
- 也可以不传形参直接使用,但在函数体内只能以event引用event对象。
- 其他
- 一个常见错误,匿名函数无法移除,详见示例。
- Edge支持该事件处理程序。
IE事件处理程序(与DOM2级事件处理程序对应)
示例代码(该代码只能在ie浏览器下运行)
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>IE事件处理程序</title>
<script type="text/javascript">
window.onload = function () {
var rootEle = document.getElementById('root');
var wrapperEle = document.getElementById('wrapper');
var innerEle = document.getElementById('inner');
rootEle.attachEvent('onclick', function () {
console.log('root listener 1', this, event);
});
wrapperEle.attachEvent('onclick', function () {
console.log('wrapper listener 1', this, event);
});
innerEle.attachEvent('onclick', function () {
console.log('inner listener 1', this, event);
});
rootEle.attachEvent('onclick', function () {
console.log('root listener 2', this, event);
});
wrapperEle.attachEvent('onclick', function () {
console.log('wrapper listener 2', this, event);
});
innerEle.attachEvent('onclick', function () {
console.log('inner listener 2', this, event);
});
rootEle.attachEvent('onclick', function () {
console.log('root listener 3', this, event);
});
wrapperEle.attachEvent('onclick', function () {
console.log('wrapper listener 3', this, event);
});
innerEle.attachEvent('onclick', function () {
console.log('inner listener 3', this, event);
});
}
</script>
</head>
<body id="root">
<div id="wrapper">
<div id="inner">
click me!
</div>
</div>
</body>
</html>
与DOM2级事件处理程序对应,ie也有两个方法分别对应添加和移除事件处理程序:
- attachEvent()
- detachEvent()
分析总结:
- 添加监听函数
- 使用 attachEvent 函数
- 第一个参数为’on-’,例如’onclick’,而不是DOM中的’click’。
- 接受两个参数。因为IE8及之前的版本只支持事件冒泡。
- 移除监听函数
- 使用 detachEvent 函数
- 参数列表要求与 attachEvent 完全一致
- 是否可以添加多个事件处理函数
- 可以,按照添加顺序相反的顺序执行。(IE8及以下)
- 可以,按照添加顺序执行。(IE9及以上,不包括Edge)
- 事件传播
- 冒泡方式调用。
- this指向
- 指向 window 对象
- event对象
- 可以传过去,即在函数参数位置写上形参。
- 也可以不传形参直接使用,但在函数体内只能以event引用event对象。
- 其他
- 同样的,匿名函数无法移除。
- 支持IE事件处理程序的浏览器有IE和Opera。
- Edge支持标准DOM2级事件处理程序,不支持IE事件处理程序。
跨浏览器的事件处理程序
1.《JavaScript高级程序设计》示例
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;
}
}
}
2.使用原型拓展
HTMLElement.prototype.addEvent = function(type, fn, capture) {
var el = this;
if (window.addEventListener) {
el.addEventListener(type, function(e) {
fn.call(el, e);
}, capture);
} else if (window.attachEvent) {
el.attachEvent("on" + type, function(e) {
fn.call(el, e);
});
}
};
注:
示例2来源于张鑫旭大神的文章漫谈js自定义事件、DOM/伪DOM自定义事件
4.事件委托
在JavaScript中,添加到页面上的事件处理程序将直接影响到页面的整体运行性能。首先,每个函数都是对象,都会占用内存;内存中的对象越多,性能就越差。其次,必须事先指定所有事件处理程序而导致的DOM访问次数,会延迟整个页面的交互就绪事件。
–《JavaScript高级程序设计》
对“事件处理程序过多”问题的解决方案就是事件委托。事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。
–《JavaScript高级程序设计》
使用场景:
在一个父节点下,有很多个子节点,需要为所有的子节点添加点击事件。一般情况下,我们会为所有的元素添加点击事件,就会有很多个重复的事件处理程序。现在我们使用事件委托,只需在DOM树中合适的节点上添加一个事件处理程序就可以了。
示例程序
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>事件委托</title>
<script type="text/javascript">
window.onload = function () {
var ulEle = document.getElementById('wrapper');
ulEle.onclick = function () {
console.log('ul DOM0');
};
ulEle.addEventListener('click', function (e) {
console.log(this, e.target, e.currentTarget);
// 目标节点
var target = e.target;
var dataID = target.getAttribute('data-id');
console.log(dataID);
}, false);
}
</script>
</head>
<body>
<ul id="wrapper">
<li data-id="a">aaaaaaaaa</li>
<li data-id="b">bbbbbbbbb</li>
<li data-id="c">ccccccccc</li>
<li data-id="d">ddddddddd</li>
<li data-id="e">eeeeeeeee</li>
</ul>
</body>
</html>
如果可行的话,也可以考虑为document对象添加一个对象处理程序,用以处理页面上发生的某种特定类型的事件。
优点如下所示:
- document对象很快就可以访问,而且可以在页面生命周期的任何时点上为它添加事件处理程序(无需等待DOMContentLoaded或load事件)。
- 在页面中设置事件处理程序所需的时间更少。
- 整个页面占用的内存空间更少,能够提升整体性能。
–《JavaScript高级程序设计》
个人而言,我比较推荐在合适的节点添加事件委托,除非有必要或者事件比较容易处理,否则不建议将所有事件都委托到document对象上。
事件委托的优点:
- 减少了声明的事件总数,减少了所需内存。
- 能够对添加事件处理程序之后再添加的节点进行处理,而不需要对后来添加的节点再次添加事件处理程序。
分析总结:
- this指向,指向事件绑定的节点,即父节点。
5.模拟事件
我们可以使用JavaScript在任意时刻来触发特定的事件,而此时的事件就如同浏览器创建的一样。
DOM中的事件模拟
第一步,使用document对象的createEvent方法创建event对象。该方法接受一个参数,即表示要创建的事件类型的字符串。
第二步,在创建了event对象之后,还需要使用与事件有关的信息对其进行初始化。不同类型的这个方法的名字也不相同,具体要取决于createEvent方法中使用的参数。
第三步,使用dispatchEvent方法触发事件,所有支持事件的DOM节点都支持这个方法。
IE中的事件模拟
第一步,使用document.createEventObject()方法可以在IE中创建event对象。但与DOM方式不同的是,这个方法不接受参数,结果会返回一个通用的event对象。
第二步,必须手工为这个对象添加必要的信息(没有方法来辅助完成这一步骤)。
第三步,就是在目标节点上调用fireEvent()方法,这个方法接受两个参数:事件处理名称和event对象。在调用fireEvent方法时,会自动为event对象添加SRCElement和type属相,其他属性都必须通过手工添加。
换句话说,模拟任何IE支持的事件都采用相同的模式。
自定义事件
DOM3级还定义了“自定义事件”。自定义事件不是由DOM原生触发的,它的目的是让开发人员创建自己的事件。
要创建新的自定义事件,可以调用createEvent(‘CustomEvent’)。返回的对象有一个名为initCustomEvent()的方法,接受如下四个参数。
- type (字符串):触发的事件类型。
- bubbles (布尔值):表示事件是否应该冒泡。
- cancelable (布尔值):表示事件是否可以取消。
- detail (对象):任意值,保存在event对象的detail属性中。
一个简单的示例
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>自定义DOM事件</title>
<script type="text/javascript">
window.onload = function () {
var event = document.createEvent('CustomEvent');
event.initCustomEvent('dbElementClick', true, false, '点击了两个子节点');
var outerEle = document.getElementById('outer');
var wrapperEle = document.getElementById('wrapper');
outerEle.addEventListener('dbElementClick', function (e) {
console.log('outer', e.detail);
}, false);
wrapperEle.addEventListener('dbElementClick', function (e) {
console.log('wrapper', e.detail);
}, false);
var firstEle;
outerEle.addEventListener('click', function (e) {
var target = e.target;
if (!firstEle) {
firstEle = target
} else if (target !== firstEle) {
outerEle.dispatchEvent(event);
}
}, false);
}
</script>
</head>
<body id="wrapper">
<div id="outer">
<div id="click1">
click me!
</div>
<div id="click2">
click me!
</div>
</div>
</body>
</html>
关于模拟事件和自定义事件,更详细的信息请参考阮一峰老师的文章或者《JavaScript高级程序设计》这本书。
6.事件对象(event)
阻止事件默认行为
event.preventDefault()
event.cancelable;
event.defaultPrevented;
其中
- preventDefault() 用来阻止事件的默认行为。
- cancelable 表示事件是否允许被取消。
- defaultPrevented 表示是否调用过 preventDefault() 方法。
- 监听事件 return false 会起到和 preventDefault() 方法一致的效果。
阻止事件传播
event.stopPropagation()
event.stopImmediatePropagation()
其中
- stopPropagation() 方法用于阻止事件在DOM上的传播,捕获或者冒泡阶段均可调用。
- stopImmediatePropagation() 有两种作用,其一该方法可以阻止事件在DOM上的传播,类似于 stopPropagation() 方法。其二,如果对一个节点定义了多个事件监听函数,那么事件监听函数将会按照顺序依次执行,使用该方法后,之后的事件处理程序都不会被触发。
事件节点
event.currentTarget
event.target
其中
- event.currentTarget 属性指向正在执行的监听函数绑定的节点。
- event.target 属性指向触发事件的节点,即事件最初发生的节点。
更多细节请参考阮一峰老师的文章事件模型或《JavaScript高级程序设计》
最后,给出这一小节的示例程序
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>DOM 事件</title>
<script type="text/javascript">
window.onload = function () {
// 阻止事件默认行为
(function () { // 请先注释该函数下面的语句
var open = document.getElementById('open');
open.addEventListener('click', function () {
event.preventDefault();
}, true);
})();
// 阻止事件传播
(function () {
var wrapperEle = document.getElementById('wrapper');
var innerEle = document.getElementById('inner');
wrapperEle.addEventListener('click', function () { // 只有这个事件会被触发
event.stopPropagation();
console.log('wrapper 捕获');
}, true);
innerEle.addEventListener('click', function () {
console.log('inner 捕获');
}, true);
wrapperEle.addEventListener('click', function () {
console.log('wrapper 冒泡');
}, false);
innerEle.addEventListener('click', function () {
console.log('inner 冒泡');
}, false);
})();
(function () {
var wrapperEle = document.getElementById('wrapper');
var innerEle = document.getElementById('inner');
wrapperEle.addEventListener('click', function () { // 这个事件会被触发
console.log('wrapper 捕获1');
}, true);
wrapperEle.addEventListener('click', function () { // 这个事件会被触发
event.stopImmediatePropagation();
console.log('wrapper 捕获2');
}, true);
wrapperEle.addEventListener('click', function () {
console.log('wrapper 捕获3');
}, true);
innerEle.addEventListener('click', function () {
console.log('inner 捕获');
}, true);
wrapperEle.addEventListener('click', function () {
console.log('wrapper 冒泡');
}, false);
innerEle.addEventListener('click', function () {
console.log('inner 冒泡');
}, false);
})();
}
</script>
</head>
<body>
<div id="wrapper">
<div id="inner">
click me!
<a id="open" href="https://www.baidu.com/" onclick="return false;">百度</a>
</div>
</div>
</body>
</html>
7.事件类型
由于事件类型繁多,本文不再赘述,详细信息请参考阮一峰老师的文章事件种类或《JavaScript高级程序设计》