1. 事件冒泡与捕获——DOM event flow
事件冒泡与捕获的过程
当一个事件发生在具有父元素的元素上时,浏览器运行2+1个阶段阶段:
- 捕获阶段:浏览器检查元素的最外层祖先,是否在捕获阶段中注册了一个onclick事件处理程序,如果是,则运行它。然后,它移动到中单击元素的下一个祖先元素,并执行相同的操作,然后是单击元素再下一个祖先元素,依此类推,直到到达实际点击的元素。
- 冒泡阶段:浏览器检查实际点击的元素是否在冒泡阶段中注册了一个onclick事件处理程序,如果是,则运行它。然后它移动到下一个直接的祖先元素,并做同样的事情,然后是下一个,等等,直到它到达元素。
- 目标阶段:实际点击target。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<title>冒泡与捕获</title>
</head>
<body>
<div id="parent">
<h2>Parent</h2>
| /\<br>
\/ |<br>
<div id="son">
<h3>Son</h3>
| /\<br>
\/ |<br>
<div id="target">
<h4>Target</h4>
</div>
</div>
</div>
<script type="text/javascript">
var parent = document.getElementById("parent");
var son = document.getElementById("son");
var target = document.getElementById("target");
parent.addEventListener("click", function (e) {
console.log("冒泡-parent");
}, false);
son.addEventListener("click", function (e) {
console.log("冒泡-son");
}, false);
target.addEventListener("click", function (e) {
console.log("冒泡-target");
}, false);
parent.addEventListener("click", function (e) {
console.log("捕获-parent");
}, true);
son.addEventListener("click", function (e) {
console.log("捕获-son");
}, true);
target.addEventListener("click", function (e) {
console.log("捕获-target");
}, true);
</script>
</body>
</html>
单击Target,其所有父级均会被触发:
单击Son,仅触发父级,其后代不会被触发:
阻止冒泡的方法
阻止冒泡的方法有两种:
- 使用stopPropagation()方法,可以阻止冒泡,但无法阻止同一事件的其他监听函数被调用,也就是说给同一事件添加的不同监听器不会被阻止;
<button id="btn" style="width: 200px;">触发</button>
<script type="text/javascript">
var btn = document.getElementById('btn');
btn.addEventListener('click',function (e){
e = e || event;
// 可以阻止冒泡,但无法阻止同一事件的其他监听函数被调用
e.stopPropagation();
this.innerHTML = '修改了';
})
//使用stopPropagation(),该函数还会执行
btn.addEventListener('click',function (e){
e = e || event;
this.style.backgroundColor = 'lightblue';
})
//使用stopPropagation(),该函数不执行
document.body.addEventListener('click',function (){
alert('body');
})
</script>
- stopImmediatePropagation()方法不仅可以取消事件的进一步捕获或冒泡,而且可以阻止同一个事件的其他监听函数被调用,无返回值;
<button id="btn" style="width: 200px;">触发</button>
<script type="text/javascript">
var btn = document.getElementById('btn');
// 可以阻止冒泡,但无法阻止同一事件的其他监听函数被调用
btn.addEventListener('click',function (e){
e = e || event;
e.stopImmediatePropagation()
this.innerHTML = '修改了';
})
// 使用stopImmediatePropagation()方法,该函数不执行
btn.addEventListener('click',function (e){
e = e || event;
this.style.backgroundColor = 'lightblue';
})
// 使用stopImmediatePropagation()方法,该函数不执行
document.body.addEventListener('click',function (){
alert('body');
})
</script>
不是所有的事件都能冒泡:
blur、focus、load和unload不能像其它事件一样冒泡。事实上blur和focus可以用事件捕获而非事件冒泡的方法获得(在IE之外的其它浏览器中)。
2. 事件委托/代理
事件委托的原理:事件冒泡。通过事件冒泡,指定一个事件处理程序来管理某一类型的所有事件。其优点为:
- 与DOM节点交互一次,减少与DOM交互次数,起到性能优化作用;
- 减少占用空间函数对象个数(仅对父级添加一个函数对象),节省内存空间;
- 新增子对象时,能动态绑定事件。
适合事件代理的事件:click,mousedown,mouseup,keydown,keyup,keypress
事件代理的实现
常规实现效果:移入li变红,移出li变白;不用事件代理时的一般写法:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>事件代理</title>
</head>
<body>
<input type="button" name="" id="btn" value="添加对象" />
<ul id="ul1" style="list-style: none;">
<li>第1个li</li>
<li>第2个li</li>
<li>第3个li</li>
<li>第4个li</li>
</ul>
<script>
var oBtn = document.getElementById("btn");
var oUl = document.getElementById("ul1");
var aLi = oUl.getElementsByTagName('li');
// for循环遍历li节点:鼠标移入变红,移出变白
for(var i=0; i<aLi.length;i++){
aLi[i].onmouseover = function(){
this.style.background = 'red';
};
aLi[i].onmouseout = function(){
this.style.background = '#fff';
}
}
// 添加新节点
oBtn.onclick = function(){
var oLi = document.createElement('li');
oLi.innerHTML = '新添加的第'+ (aLi.length+1)+'个li';
oUl.appendChild(oLi);
};
</script>
</body>
</html>
在一般写法中,新增加的li
无法实现需要的效果,也就是说没能绑定事件,说明添加子节点的时候,事件没有一起添加进去。
一般的解决方案可以将将for循环命名为一个函数,命名为一个函数,能够实现我们想要的效果,具体如下:
<input type="button" name="" id="btn" value="添加对象" />
<ul id="ul1" style="list-style: none;">
<li>第1个li</li>
<li>第2个li</li>
<li>第3个li</li>
<li>第4个li</li>
</ul>
<script>
var oBtn = document.getElementById("btn");
var oUl = document.getElementById("ul1");
var aLi = oUl.getElementsByTagName('li');
function mHover() {
// for循环遍历
for (var i = 0; i < aLi.length; i++) {
aLi[i].onmouseover = function () {
this.style.background = 'red';
};
aLi[i].onmouseout = function () {
this.style.background = '#fff';
}
}
}
mHover();
//添加新节点
oBtn.onclick = function () {
var oLi = document.createElement('li');
oLi.innerHTML = '新添加的第' + (aLi.length + 1) + '个li';
oUl.appendChild(oLi);
mHover();
};
</script>
能实现,但实际上无疑是又增加了一个dom操作,在优化性能方面是不可取的,那么有事件委托的方式,新添加的子元素是带有事件效果的,当用事件委托的时候,根本就不需要去遍历元素的子节点,只需要给父级元素添加事件,其他的都是在js里面的执行,大大的减少dom操作,如下:
<input type="button" name="" id="btn" value="添加对象" />
<ul id="ul1" style="list-style: none;">
<li>第1个li</li>
<li>第2个li</li>
<li>第3个li</li>
<li>第4个li</li>
</ul>
<script>
var oBtn = document.getElementById("btn");
var oUl = document.getElementById("ul1");
var aLi = oUl.getElementsByTagName('li');
oUl.onmouseover = function (ev) {
var ev = ev || window.event; // 获取当前事件源
var target = ev.target || ev.srcElement; // 兼容IE和标准浏览器
// 判断目标标签是否为li,防止点击ul出发事件
if (target.nodeName.toLowerCase() == 'li') {
target.style.background = "red";
}
};
oUl.onmouseout = function (ev) {
var ev = ev || window.event; // 获取当前事件源
var target = ev.target || ev.srcElement; // 兼容IE和标准浏览器
// 判断目标标签是否为li,防止点击ul出发事件
if (target.nodeName.toLowerCase() == 'li') {
target.style.background = "#fff";
}
};
//添加新节点
oBtn.onclick = function () {
var oLi = document.createElement('li');
oLi.innerHTML = '新添加的第' + (aLi.length + 1) + '个li';
oUl.appendChild(oLi);
};
</script>
这里用父级ul做事件处理,当li被点击时,由于冒泡原理,事件就会冒泡到ul上,因为ul上有点击事件,所以事件就会触发。
但是如果在li
标签内添加其他标签,则无法达到效果,因此,进一步可以改进实现:
<input type="button" name="" id="btn" value="添加对象" />
<ul id="ul1" style="list-style: none;">
<li>第1个li<span>内容123</span></li>
<li>第2个li<span>内容123</span></li>
<li>第3个li<span>内容123</span></li>
<li>第4个li<span>内容123</span></li>
</ul>
var uldom2 = document.getElementById("ul1");
delegate(uldom2, "click", "li", fn);
function delegate(element, eventType, selector, fn) {
element.addEventListener(
eventType,
(e) => {
let el = e.target;
while (!el.matches(selector)) {
if (element === el) {
el = null;
break;
}
el = el.parentNode;
}
el && fn.call(el, e, el);
},
true
);
return element;
}