DOM级别一共可以分为四个级别:DOM0级、DOM1级、DOM2级和DOM3级。而DOM事件分为3个级别:DOM 0级事件处理,DOM 2级事件处理和DOM 3级事件处理。由于DOM 1级中没有事件的相关内容,所以没有DOM 1级事件。
DOM0级事件之前,我们有必要了解一下HTML事件处理程序,也是最早的一种事件处理方式:
<button type="button" onclick="showFn()">button>
function showFn() {
alert('Hello World');
}
以上的代码,我们通过直接在HTML代码里面定义了一个 onclick属性触发showfu这样的事件处理程序最大的缺点就是HTML和JS耦合太强,我们如果需要修改函数名就必须修改两个地方,优点是不需要操作DOM来完成事件的绑定
DOM0级处理事件就是将一个函数赋值给一个事件处理属性:
el.οnclick=function(){}
let btn = document.getElementById('btn');
btn.onclick = function () {
alert(this.innerHTML);
}
当希望为同一个元素/标签绑定多个同类型事件的时候(如给上面的这个btn元素绑定3个点击事件),是不被允许的。DOM0事件绑定,给元素的事件行为绑定方法,这些方法都是在当前元素事件行为的冒泡阶段(或者目标阶段)执行的。
DOM2级事件在DOM0级事件基础上弥补了一个处理处理程序无法同时绑定多个处理函数的缺点。允许给一个程序添加多个处理函数。
DOM2级事件定义了addEventListener 和 removeEventListener两个方法,分别用来绑定和解绑事件。
el.addEventListener(event-name, callback, useCapture)
let btn = document.getElementById('btn');
btn.addEventListener('click', test, false);
function test (e) {
e = e || window.event;
alert((e.target || e.srcElement).innerHTML);
btn.removeListener('click', test);
}
//IE9-:attachEvent()与detachEvent()。
//IE9+/chrom/FF:addEventListener()和removeEventListener()
需要注意的是IE8以下版本不支持 addEventlistener 和 removeEventListerner需要使用attachEvent和detachEvent实现。
btn.attachEvent('onclick', showFn); // 绑定事件
btn.detachEvent('onclick', showFn); // 解绑事件
这里我们不需要传入第三个参数,因为IE8以下版本只支持冒泡型事件。
在DOM 2级事件的基础上添加了更多的事件类型。
同时DOM3级事件也允许使用者自定义一些事件。
DOM事件模型分为捕获和冒泡。一个事件发生后,会在子元素和父元素之间传播(propagation)。这种传播分成三个阶段。
上文中讲到了addEventListener的第三个参数为指定事件是否在捕获阶段执行,设置为true表示事件在捕获阶段执行,而设置为false表示事件在冒泡阶段执行。那么什么是事件冒泡和事件捕获呢?可以用下图来解释:
所谓事件冒泡就是事件像泡泡一样从最开始生成的地方一层一层往上冒,比如上图中a标签为事件目标,点击a标签后同时也会触发p、li上的点击事件,一层一层向上直至最外层的html或document。
如下事件冒泡的例子:
// 例3
......
window.onclick = function() {
console.log('window');
};
document.onclick = function() {
console.log('document');
};
document.documentElement.onclick = function() {
console.log('html');
};
document.body.onclick = function() {
console.log('body');
}
outer.onclick = function(ev) {
console.log('outer');
};
inner.onclick = function(ev) {
console.log('inner');
};
正如我们上面提到的onclick给元素的事件行为绑定方法都是在当前元素事件行为的冒泡阶段(或者目标阶段)执行的。
那么我们如何阻止事件冒泡呢?这里就涉及事件的Event对象中的stopPropagation方法,后文有具体讲解。
和事件冒泡相反,事件捕获是自上而下执行,我们只需要将addEventListener的第三个参数改为true就行。
<div id="box">
<a id="child">事件冒泡a>
div>
<script>
var box = document.getElementById('box'),
child = document.getElementById('child');
child.addEventListener('click', function() {
alert('我是目标事件');
}, true);
box.addEventListener('click', function() {
alert('事件冒泡至DIV');
}, true);
script>
此时我们点击a标签,首先弹出的是’事件冒泡至DIV’,其次弹出的是’我是目标事件’,正好与事件冒泡相反。
由于事件会在冒泡阶段向上传播到父节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件。这种方法叫做事件的代理(delegation)。
假设有一个列表,列表之中有大量的列表项,我们需要在点击每个列表项的时候响应一个事件
// 例4
<ul id="list">
<li>item 1li>
<li>item 2li>
<li>item 3li>
......
<li>item nli>
ul>
如果给每个列表项一一都绑定一个函数,那对于内存消耗是非常大的,效率上需要消耗很多性能。借助事件代理,我们只需要给父容器ul绑定方法即可,这样不管点击的是哪一个后代元素,都会根据冒泡传播的传递机制,把容器的click行为触发,然后把对应的方法执行,根据事件源,我们可以知道点击的是谁,从而完成不同的事。
在很多时候,我们需要通过用户操作动态的增删列表项元素,如果一开始给每个子元素绑定事件,那么在列表发生变化时,就需要重新给新增的元素绑定事件,给即将删去的元素解绑事件,如果用事件代理就会省去很多这样麻烦。
接下来我们来实现上例中父层元素 #list 下的 li 元素的事件委托到它的父层元素上:
// 给父层元素绑定事件
document.getElementById('list').addEventListener('click', function (e) {
// 兼容性处理
var event = e || window.event;
var target = event.target || event.srcElement;
// 判断是否匹配目标元素
if (target.nodeName.toLocaleLowerCase === 'li') {
console.log('the content is: ', target.innerHTML);
}
});
如果调用这个方法,默认事件行为将不再触发。什么是默认事件呢?例如表单一点击提交按钮(submit)跳转页面、a标签默认页面跳转或是锚点定位等。
很多时候我们使用a标签仅仅是想当做一个普通的按钮,点击实现一个功能,不想页面跳转,也不想锚点定位。
//方法一:
<a href="javascript:;">链接a>
也可以通过JS方法来阻止,给其click事件绑定方法,当我们点击A标签的时候,先触发click事件,其次才会执行自己的默认行为
//方法二:
<a id="test" href="http://www.cnblogs.com">链接a>
<script>
test.onclick = function(e){
e = e || window.event;
return false;
}
script>
//方法三:
<a id="test" href="http://www.cnblogs.com">链接a>
<script>
test.onclick = function(e){
e = e || window.event;
e.preventDefault();
}
script>
接下来我们看个例子:输入框最多只能输入六个字符,如何实现?
// 例5
<input type="text" id='tempInp'>
<script>
tempInp.onkeydown = function(ev) {
ev = ev || window.event;
let val = this.value.trim() //trim去除字符串首位空格(不兼容)
// this.value=this.value.replace(/^ +| +$/g,'') 兼容写法
let len = val.length
if (len >= 6) {
this.value = val.substr(0, 6);
//阻止默认行为去除特殊按键(DELETE\BACK-SPACE\方向键...)
let code = ev.which || ev.keyCode;
if (!/^(46|8|37|38|39|40)$/.test(code)) {
ev.preventDefault()
}
}
}
script>
event.stopPropagation()
方法阻止事件冒泡到父元素,阻止任何父事件处理程序被执行。上面提到事件冒泡阶段是指事件从目标节点自下而上向window对象传播的阶段。
我们在例3的inner元素click事件上,添加event.stopPropagation()
这句话后,就阻止了父事件的执行,最后只打印了’inner’。
inner.onclick = function(ev) {
console.log('inner');
ev.stopPropagation();
};
stopImmediatePropagation 既能阻止事件向父元素冒泡,也能阻止元素同事件类型的其它监听器被触发。而 stopPropagation 只能实现前者的效果。我们来看个例子:
......
const btn = document.querySelector('#btn');
btn.addEventListener('click', event => {
console.log('btn click 1');
event.stopImmediatePropagation();
});
btn.addEventListener('click', event => {
console.log('btn click 2');
});
document.body.addEventListener('click', () => {
console.log('body click');
});
// btn click 1
如上所示,使用 stopImmediatePropagation后,点击按钮时,不仅body绑定事件不会触发,与此同时按钮的另一个点击事件也不触发。
老实说这两者的区别,并不好用文字描述,我们先来看个例子:
<div id="a">
<div id="b">
<div id="c"><div id="d">div>div>
div>
div>
<script>
document.getElementById('a').addEventListener('click', function(e) {
console.log(
'target:' + e.target.id + '¤tTarget:' + e.currentTarget.id
)
})
document.getElementById('b').addEventListener('click', function(e) {
console.log(
'target:' + e.target.id + '¤tTarget:' + e.currentTarget.id
)
})
document.getElementById('c').addEventListener('click', function(e) {
console.log(
'target:' + e.target.id + '¤tTarget:' + e.currentTarget.id
)
})
document.getElementById('d').addEventListener('click', function(e) {
console.log(
'target:' + e.target.id + '¤tTarget:' + e.currentTarget.id
)
})
script>
当我们点击最里层的元素d的时候,会依次输出:
target:d¤tTarget:d
target:d¤tTarget:c
target:d¤tTarget:b
target:d¤tTarget:a
从输出中我们可以看到,event.target
指向引起触发事件的元素,而event.currentTarget
则是事件绑定的元素,只有被点击的那个目标元素的event.target
才会等于event.currentTarget
。也就是说,event.currentTarget
始终是监听事件者,而event.targe
t是事件的真正发出者。