1.DOM事件级别
DOM级别可以分成0、1、2、3四个级别,但是只有0、2、3级别中有事件相关的内容,没有DOM 1级事件。
1.1 DOM 0级事件
1.1 行内元素
<input type="button" id="btn" value="按钮" onclick="alert('行内事件')"></input>
1.2 element.on事件名 = function(){}
例如element.onclick = function(){}
var btn = document.getElementById('btn');
btn.onclick = function () {
alert(this.innerHTML);
}
缺点是一个处理事件只能绑定一个函数,例如想为onclick绑定几个函数是不被允许的。
元素事件行为绑定的方法都是在当前元素事件行为的冒泡阶段(或者目标阶段)执行的。
1.2 DOM 2级事件
DOM 2级事件就是监听方法:addEventListener()和removeEventListener()
监听方法具有3个参数:
el.addEventListener(event-name, callback, useCapture)
- event-name:事件名称
- callback:回调函数,当事件触发时,函数会被传入一个参数为当前的事件对象event
- useCapture:默认是false,表示元素在事件的冒泡阶段响应事件;设为true则表示元素在事件的捕获阶段响应事件。
IE9以下的IE浏览器不支持 addEventListener()和removeEventListener(),使用 attachEvent()与detachEvent() 代替,因为IE9以下是不支持事件捕获的,所以也没有第三个参数,第一个事件名称前要加on。
1.3 DOM 3级事件
在DOM 2级事件基础上添加了更多事件类型
- UI事件,当用户与页面上的元素交互时触发,如:load、scroll
- 焦点事件,当元素获得或失去焦点时触发,如:blur、focus
- 鼠标事件,当用户通过鼠标在页面执行操作时触发如:dbclick、mouseup
- 滚轮事件,当使用鼠标滚轮或类似设备时触发,如:mousewheel
- 文本事件,当在文档中输入文本时触发,如:textInput
- 键盘事件,当用户通过键盘在页面上执行操作时触发,如:keydown、keypress
- 合成事件,当为IME(输入法编辑器)输入字符时触发,如:compositionstart
- 变动事件,当底层DOM结构发生变化时触发,如:DOMsubtreeModified
- 一些使用者自定义的事件
2. DOM事件模型和事件流
2.1 概述
DOM事件模型分为捕获和冒泡,一个事件发生后,会在子元素和父元素之间传播,这种传播分成3个阶段:
(1)捕获阶段:事件从window对象自上而下向目标节点传播的阶段;
window -> document -> html -> body -> ...
(2)目标阶段:真正的目标节点正在处理事件的阶段;
(3)冒泡阶段:事件从目标节点自下而上向window对象传播的阶段。
2.2 具体流程
事件冒泡
<div id="parent">
fatherElement
<div id="child">
childElement
</div>
</div>
<script type="text/javascript">
var parent = document.getElementById("parent");
var child = document.getElementById("child");
document.body.addEventListener("click", function (e) {
console.log("click-body");
}, false);
parent.addEventListener("click", function (e) {
console.log("click-parent");
}, false);
child.addEventListener("click", function (e) {
console.log("click-child");
}, false);
//点击child的div输出:
//click-child
//click-parent
//click-body
</script>
事件捕获,只要将上面addEventListener的第三个参数改为true,再次点击child的div输出会变成:
//click-body
//click-parent
//click-child
正如上面提到的,onclick给元素绑定方法都是在冒泡阶段(或者目标阶段)执行的。
<div id="outer">
<div id="inner"></div>
</div>
<script type="text/javascript">
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');
};
//inner -> outer -> body -> html -> document -> window
</script>
2.3 阻止冒泡
2.3.1 event.stopPropagation()
该方法可以阻止事件冒泡到父元素。
还是2.2中的例子,在child的监听函数里阻止冒泡:
var parent = document.getElementById("parent");
var child = document.getElementById("child");
document.body.addEventListener("click", function (e) {
console.log("click-body");
}, false);
parent.addEventListener("click", function (e) {
console.log("click-parent");
}, false);
child.addEventListener("click", function (e) {
event.stopPropagation();
console.log("click-child");
}, false);
//点击child的div输出:
//click-child
2.3.2 event.stopImmediateProgation()
和stopPropagation()作用相同点在于:用于阻止事件冒泡到父元素。
不同点在于,该方法阻止监听同一事件的其它事件监听器被调用。
如果多个事件监听器被附加到相同元素的相同事件类型上,如果在其中一个监听器内执行该方法,剩下的事件监听器都不会被调用。
比如这个例子:
<div>
<p>paragraph</p>
</div>
<script>
const p = document.querySelector('p')
p.addEventListener("click", (event) => {
alert("我是 p 元素上被绑定的第一个监听函数");
}, false);
p.addEventListener("click", (event) => {
alert("我是 p 元素上被绑定的第二个监听函数");
event.stopImmediatePropagation();
// 执行 stopImmediatePropagation 方法,阻止 click 事件冒泡,并且阻止 p 元素上绑定的其他 click 事件的事件监听函数的执行。
}, false);
p.addEventListener("click", (event) => {
alert("我是 p 元素上被绑定的第三个监听函数");
// 该监听函数排在上个函数后面,该函数不会被执行
}, false);
document.querySelector("div").addEventListener("click", (event) => {
alert("我是 div 元素,我是 p 元素的上层元素");
// p 元素的 click 事件没有向上冒泡,该函数不会被执行
}, false);
</script>
如果点击p元素,则只会打印前两个监听函数。不打印第三个监听函数是因为被stopImmediatePropagation()拦截了,没有向上冒泡到div是因为阻止冒泡了。
如果把上面的方法换成stopPropagation()
p.addEventListener("click", (event) => {
console.log("我是 p 元素上被绑定的第二个监听函数");
event.stopPropagation();
// event.stopImmediatePropagation();
}, false);
那么点击p元素则会打印3个监听函数,但是因为阻止了冒泡的缘故不会打印div。
2.3.3 event.preventDefault()
该方法和阻止冒泡没什么关系,不过还是放到一起写了。
该方法的主要作用是阻止某个事件的默认动作被正常执行。比如阻止点击链接跳转,阻止复选框被点击后变成checked状态等等。
经过处理后的事件还是正常传播的,除非碰到阻止冒泡的那两个方法。
<p>Please click on the checkbox control.</p>
<form>
<label for="id-checkbox">Checkbox:</label>
<input type="checkbox" id="id-checkbox" />
</form>
<div id="output-box"></div>
<script>
let box = document.querySelector('#id-checkbox');
box.addEventListener('click' , function(event){
let output = document.querySelector('#output-box');
event.preventDefault();
output.innerHTML = "<code>preventDefault()</code> won't let you check this<br>"
} , false);
</script>
比如只允许输入框输入6个字符
<input type="text" id='tempInp'>
<script>
tempInp.onkeydown = function (ev) {
ev = ev || window.event;
let val = this.value.trim() //trim去除字符串首位空格(不兼容)
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>
3. 事件委托
由于事件会在冒泡阶段向上传播到父节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理子节点的事件。 这种处理方式称为事件委托。
事件委托主要有两个好处。
3.1 减少内存消耗,提高性能
比如实现当鼠标移到li上就能改变背景颜色
首先可以通过for循环遍历每个li实现
<ul id='ul'>
<li>item1</li>
<li>item2</li>
<li>item3</li>
</ul>
<script>
window.onload = function () {
let li = document.querySelectorAll("li");
for (var i = 0; i < li.length; i++) {
li[i].onmouseover = function () {
this.style.background = "red";
}
li[i].onmouseout = function () {
this.style.background = "";
}
}
}
</script>
通过事件委托方式实现
<script>
window.onload = function () {
let ul = document.querySelector('#ul');
ul.onmouseover = function(e){
let target = e.target;
if(target.nodeName.toLowerCase() === 'li'){
target.style.background = 'red'
}
}
ul.onmouseout = function(e){
let target = e.target;
if(target.nodeName.toLowerCase() === 'li'){
target.style.background = ''
}
}
}
</script>
3.2 动态绑定事件
在很多时候,我们需要通过用户操作动态的增删列表项元素,如果一开始给每个子元素绑定事件,那么在列表发生变化时,就需要重新给新增的元素绑定事件,给即将删去的元素解绑事件。
使用事件代理来解决:比如要动态给ul添加li,不使用事件委托,会发现新增的li背景颜色不会被改变,因为点击添加时for循环已经执行完毕。
<input type="button" id="btn" value="add item">
<ul id='ul'>
<li>item1</li>
<li>item2</li>
<li>item3</li>
</ul>
<script>
window.onload = function () {
var ul = document.getElementById("ul");
var li = ul.getElementsByTagName("li");
var btn = document.getElementById("btn");
var iNow = 3;
for (var i = 0; i < li.length; i++) {
li[i].onmouseover = function () {
this.style.background = "red";
}
li[i].onmouseout = function () {
this.style.background = "";
}
}
btn.onclick = function () {
iNow++;
var li = document.createElement("li");
li.innerHTML = `item${iNow}`;
ul.appendChild(li);
}
}
</script>
使用事件委托来做,就可以实现新增子对象时,无需再对其进行事件绑定。(因为事件绑在父对象上)
<script>
window.onload = function () {
let ul = document.querySelector('#ul');
let btn = document.querySelector('#btn');
let cnt = 3;
ul.onmouseover = (e) =>{
let target = e.target;
if(target.nodeName.toLowerCase() === 'li'){
target.style.background = 'red';
}
}
ul.onmouseout = (e) =>{
let target = e.target;
if(target.nodeName.toLowerCase() === 'li'){
target.style.background = '';
}
}
btn.onclick = () =>{
cnt++;
let li = document.createElement('li');
li.innerHTML = `item${cnt}`;
ul.appendChild(li)
}
}
</script>
3.3 点击ul中的li
<body>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>
<script>
const ul = document.querySelector('ul');
ul.addEventListener('click' , function(e) {
console.log(e.target);
})
</script>
</body>
4. event.target & event.currentTarget
常见的两个阻止冒泡的方法和阻止默认事件的方法上面已经写了,这里写上剩下的两个常用方法的区别
以这个作为例子
<div id="a">
<div id="b">
<div id="c"></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
)
})
</script>
点击最内层c元素时,输出如下:
target:c & currentTarget:c
target:c & currentTarget:b
target:c & currentTarget:a
从输出中我们可以知道:
- event.target指向引起触发事件的元素
- event.currentTarget指向事件绑定的元素,只有在被点击的那个元素两者才会相等(this和该属性一样,都是指向被绑定的元素)
- 也就是说前者是事件的真正发出者;后者是事件的监听者
如果把几个监听器的第三个属性改为true,则事件变成捕获机制执行,同样点击c,输出会变成:
target:c & currentTarget:a
target:c & currentTarget:b
target:c & currentTarget:c
5.addEventListener & onclick
两者主要有以下不同点:
(1) 前者可以绑定并按顺序执行多个事件;后者只能绑定一个事件,若绑定了多个,只执行最后一个。
(2) 前者可以指定是执行捕获机制还是冒泡机制;后者只能执行冒泡机制
(3) 前者对于任何DOM元素都有效;而后者仅对HTML DOM元素有效
DOM包括核心DOM,XML DOM和HTML DOM:
核心DOM提供了操作文档的公有属性和方法,就相当于鼻祖。它可以可操作一切结构化文档的API,包括HTML和XML。全面但是繁琐。
HTML DOM专门操作HTML文档的简化版DOMAPI仅对常用的复杂的API进行了简化,对核心DOM进行了在HTML方面的拓展。不是万能的,但是简单
XML DOM提供了所有XML元素的对象和属性,以及访问方法,与HTML DOM类似。
(4) 前者注册事件时不需要加on,写'click'即可
(5) 移除事件时,前者通过removeListener;后者通过document.onclick = null