冒泡与捕获事件介绍

事件冒泡

IE 事件流被称为事件冒泡,这是因为事件被定义为从最具体的元素(文档树中最深的节点)开始触发,然后向上传播至没有那么具体的元素(文档)—-《JavaScript高级程序设计第四版》

举个例子:

#outer {
width: 120px;
height: 90px;
background: pink;
}

#father {
margin-left: 10px;
width: 100px;
height: 60px;
background: skyblue;
}

#son {
margin-left: 10px;
width: 80px;
height: 30px;
background: tomato;
}

<div id="outer">我是父亲
<div id="father">我是儿子
<div id="son">我是孙子</div>
</div>
</div>

let outer = document.getElementById('outer')
let father = document.getElementById('father')
let son = document.getElementById('son')

outer.addEventListener('click', function () {
console.log('我是父亲')
})
father.addEventListener('click', function () {
console.log('我是儿子')
})
son.addEventListener('click', function () {
console.log('我是孙子')
})

当我们点击 “我是孙子” 时,页面上会依次输出:我是孙子、我是儿子、我是父亲。在事件冒泡中 “我是孙子” 是最先点击的,所以也就最先触发了,然后在沿着 DOM 树向上层节点依次传递事件。然后就输出了刚刚的效果。

其实我们只给三个 div 设置了点击事件,让我们可以清晰的看到是以冒泡的形式来触发事件的,而在 DOM 节点中真实的样子是:我是孙子、我是儿子、我是父亲、body、html、Document。

注意:所有的现代浏览器都支持事件冒泡。IE5.5及早期版本会跳过元素(从直接到 document)。

事件捕获

事件捕获的意思是最不具体的节点应该最先收到事件,而最具体的节点应该最后收到事件。事件捕获实际上是为了在事件到达最终目标前拦截事件。—-《JavaScript高级程序设计第四版》

也就是前面事件冒泡的例子如果用的是事件捕获,那么点击 “我是孙子” 页面会依次输出:我是父亲、我是儿子、我是孙子。

而在 DOM 节点中真实的样子是:Document、html、body、我是父亲、我是儿子、我是孙子。

#outer {
width: 120px;
height: 90px;
background: pink;
}

#father {
margin-left: 10px;
width: 100px;
height: 60px;
background: skyblue;
}

#son {
margin-left: 10px;
width: 80px;
height: 30px;
background: tomato;
}

<div id="outer">我是父亲
<div id="father">我是儿子
<div id="son">我是孙子</div>
</div>
</div>

let outer = document.getElementById('outer')
let father = document.getElementById('father')
let son = document.getElementById('son')

outer.addEventListener('click', function () {
console.log('我是父亲')
}, true)
father.addEventListener('click', function () {
console.log('我是儿子')
}, true)
son.addEventListener('click', function () {
console.log('我是孙子')
}, true)

你也看到了,也就是在 addEventListener() 函数中配置一个布尔值,那加与不加有什么区别呢?就是使用冒泡事件还是捕获事件。

addEventListener() 函数默认是使用冒泡事件,为 false。如果想使用捕获事件,那么请将布尔值设置为 true。

如果想回顾一下这个函数,可以回顾MDN,或者看我后面的介绍。

事件代理

事件代理(事件委托):原理就是利用事件冒泡。只指定一个事件处理程序,就可以管理某一类型的所有事件。

我任务优点:
1.可以大量节省内存占用,减少事件注册。
2.可以实现当新增子对象时,无需再对其进行事件绑定,对于动态内容部分尤为合适

缺点:事件代理的常用应用应该仅限于上述需求,如果把所有事件都用事件代理,可能会出现事件误判。即本不该被触发的事件被绑定上了事件。

最普遍做法点击li打印事件:

window.onload = function(){
var oUl = document.getElementById("ul");
var aLi = oUl.getElementsByTagName('li');
for(var i = 0;i < aLi.length;i++){
aLi[i].onclick = function(){
alert(123);
}
}
}
s
// 用事件委托这么做
window.onload = function(){
  var oUl = document.getElementById("ul1");
  oUl.onclick = function(ev){
    var ev = ev || window.event;
    var target = ev.target || ev.srcElement;
    if(target.nodeName.toLowerCase() == 'li'){
         alert(123);
        alert(target.innerHTML);
    }
  }
}

这里借用了 博主中文章的例子

<div id="box">
<input type="button" id="add" value="添加" />
<input type="button" id="remove" value="删除" />
<input type="button" id="move" value="移动" />
<input type="button" id="select" value="选择" />
</div>

// 通常写法
window.onload = function(){
var Add = document.getElementById("add");
var Remove = document.getElementById("remove");
var Move = document.getElementById("move");
var Select = document.getElementById("select");

Add.onclick = function(){
alert('添加');
};
Remove.onclick = function(){
alert('删除');
};
Move.onclick = function(){
alert('移动');
};
Select.onclick = function(){
alert('选择');
}

}

// 运用事件代理
window.onload = function(){
var oBox = document.getElementById("box");
oBox.onclick = function (ev) {
var ev = ev || window.event;
var target = ev.target || ev.srcElement;
if(target.nodeName.toLocaleLowerCase() == 'input'){
switch(target.id){
case 'add' :
alert('添加');
break;
case 'remove' :
alert('删除');
break;
case 'move' :
alert('移动');
break;
case 'select' :
alert('选择');
break;
}
}
}

}

点击新增元素节点

<input type="button" name="" id="btn" value="添加" />
<ul id="ul1">
<li>111</li>
<li>222</li>
<li>333</li>
<li>444</li>
</ul>

// 通常写法
window.onload = function(){
var oBtn = document.getElementById("btn");
var oUl = document.getElementById("ul1");
var aLi = oUl.getElementsByTagName('li');
var num = 4;

function mHover () {
//鼠标移入变红,移出变白
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(){
num++;
var oLi = document.createElement('li');
oLi.innerHTML = 111*num;
oUl.appendChild(oLi);
// 必须调用 mHover 函数才能将事件添加到新节点中
mHover ();
};
}

// 使用事件委托方案
window.onload = function(){
var oBtn = document.getElementById("btn");
var oUl = document.getElementById("ul1");
var aLi = oUl.getElementsByTagName('li');
var num = 4;

//事件委托,添加的子元素也有事件
oUl.onmouseover = function(ev){
var ev = ev || window.event;
var target = ev.target || ev.srcElement;
if(target.nodeName.toLowerCase() == 'li'){
target.style.background = "red";
}

};
oUl.onmouseout = function(ev){
var ev = ev || window.event;
var target = ev.target || ev.srcElement;
if(target.nodeName.toLowerCase() == 'li'){
target.style.background = "#fff";
}

};

//添加新节点
oBtn.onclick = function(){
num++;
var oLi = document.createElement('li');
oLi.innerHTML = 111*num;
oUl.appendChild(oLi);
};
}

给一个场景 ul > li > div > p,div占满li,p占满div,还是给ul绑定时间,需要判断点击的是不是li(假设li里面的结构是不固定的),那么e.target就可能是p,也有可能是div,这种情况你会怎么处理呢?

<ul id="test">
<li>
<p>11111111111</p>
</li>
<li>
<div>
22222222
</div>
</li>
<li>
<span>3333333333</span>
</li>
<li>4444444</li>
</ul>

var oUl = document.getElementById('test');
oUl.addEventListener('click',function(ev){
var target = ev.target;
while(target !== oUl ){
if(target.tagName.toLowerCase() == 'li'){
console.log('li click~');
break;
}
target = target.parentNode;
}
})

addEventListener() 函数

addEventListener()和 removeEventListener() 给 DOM 节点添加或移除事件。共有三个参数:事件名、事件处理函
数和一个布尔值。true – 采用捕获事件,false(默认值)采用冒泡事件。

使用此函数可以设置多个事件。他们会按顺序执行。

例子可以看上面的 冒泡事件 和 捕获事件 的操作代码。当然这里也有很简单的使用。

let btn1 = document.getElementById("btn1"); 
let btn2 = document.getElementById("btn2");
// 采用冒泡事件,也可以不写 false
btn1.addEventListener("click", () => {
console.log('哈哈');
}, false);

// 采用捕获事件
btn2.addEventListener("click", () => {
console.log('哈哈');
}, true);

注意:通过 addEventListener() 添加的事件处理程序只能使用 removeEventListener() 并传入与添加时同样的参数来移除。

let btn1 = document.getElementById("btn1"); 
let btn2 = document.getElementById("btn2");

btn1.addEventListener("click", function() {
console.log('btn1');
}, false);

btn2.addEventListener("click", function() {
console.log('btn2');
}, false);

// 移除 -- 有效果
btn1.removeEventListener("click", function() {
console.log('btn1');
}, false);

// 移除 -- 无效果
btn2.removeEventListener("click", function() {
console.log('这是无效移除,因为函数不同');
}, false);

一个很离谱的浏览器(你懂的)

此浏览器需要使用 attachEvent() 和 detachEvent() 来实现添加和移除事件。他们接收两个同样的参数:事件名字和事件处理函数。

注意:IE8 及更早版本只支持事件冒泡,所以使用 attachEvent() 添加的事件处理程序会添加到冒泡阶段。

var btn = document.getElementById("myBtn"); 
btn.attachEvent("onclick", function() {
console.log("Clicked");
});

btn.attachEvent("onclick", function() {
console.log("Hello world!");
});

同样的可以设置多个事件,但这里的执行顺序是反向触发,也就是先输出”Hello world!”,然后再输出”Clicked”。

注意:

  1. attachEvent() 的第一个参数是”onclick”,而不是 DOM 的 addEventListener() 方法的”click”。

  2. 使用 attachEvent() 时,事件处理程序是在全局作用域中运行的,因此 this 等于 window。

兼容所有浏览器

function addHandler(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 removeHandler(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;
}
}