浏览器事件模型
浏览器事件模型中的过程主要分为三个阶段:捕获阶段、目标阶段、冒泡阶段。
如何绑定事件
我们有三种方法可以绑定事件,分别是行内绑定,直接赋值,用addEventListener
行内:
<button onclick="handleClick()">Press me</button>
上面这种方法不推荐。
直接赋值
var btn = document.querySelector('button');
btn.onclick = function() {
console.log('button clicked')
}
上面这种方法不推荐,原因: 这种方法有两个缺点
- 不能添加多个同类型的handler
- 不能控制在哪个阶段来执行,这个会在后面将事件捕获/冒泡的时候讲到。这个同样可以通过addEventListener来解决。 因此addEventListener横空出世,这个也是目前推荐的写法。
addEventListener
window.addEventListener("click", function (e) {
console.log("window 捕获", e.target.nodeName, e.currentTarget.nodeName);
}, true);
它有三个参数:事件名称type、事件具体操作function、捕获/冒泡
捕获:事件流从上而下,假设有三个元素,parent、child、son,如果我点击了son元素,那么parent、child上的元素也会执行。
冒泡:事件流从下而上
const parent = document.getElementById("parent");
const child = document.getElementById("child");
const son = document.getElementById("son");
window.addEventListener("click", function (e) {
console.log("window 捕获", e.target.nodeName, e.currentTarget.nodeName);
}, true);
parent.addEventListener("click", function (e) {
console.log("parent 捕获", e.target.nodeName, e.currentTarget.nodeName);
}, true);
child.addEventListener("click", function (e) {
console.log("child 捕获", e.target.nodeName, e.currentTarget.nodeName);
}, true);
son.addEventListener("click", function (e) {
console.log("son 捕获", e.target.nodeName, e.currentTarget.nodeName);
}, true);
window.addEventListener("click", function (e) {
console.log("window 冒泡", e.target.nodeName, e.currentTarget.nodeName);
}, false);
打印:
注意:
- 当第三个参数不传时,默认为false,即冒泡。
- 考虑浏览器的兼容性,ie浏览器是没有addEventListener的。分别是attachEvent、detachEvent。
- function的参数e含义:e.target.nodeName 指当前点击的元素, e.currentTarget.nodeName绑定监听事件的元素
阻止事件传播
- e.stopPropagation()
大家经常听到的可能是阻止冒泡,实际上这个方法不只能阻止冒泡,还能阻止捕获阶段的传播。
- stopImmediatePropagation() 如果有多个相同类型事件的事件监听函数绑定到同一个元素,当该类型的事件触发时,它们会按照被添加的顺序执行。如果其中某个监听函数执行了 event.stopImmediatePropagation() 方法,则当前元素剩下的监听函数将不会被执行。
function stopPropagation(ev) {
if (ev.stopPropagation) {
ev.stopPropagation(); // 标准w3c
} else {
ev.cancelBubble = true; // IE
}
}
注意:ie不支持事件捕获,它也只能阻止事件冒泡,但是标准w3c是可以阻止捕获的。
取消事件的默认行为
- e.preventDefault()
e.preventDefault()可以阻止事件的默认行为发生,默认行为是指:点击a标签就转跳到其他页面、拖拽一个图片到浏览器会自动打开、点击表单的提交按钮会提交表单等等,因为有的时候我们并不希望发生这些事情,所以需要阻止默认行为
function preventDefault(event) {
if (event.preventDefault) {
event.preventDefault(); // 标准w3c
} else {
event.returnValue = false; // IE
}
}
注意:主要是考虑浏览器的兼容性问题。
- attachEvent——兼容:IE7、IE8; 不支持第三个参数来控制在哪个阶段发生,默认是绑定在冒泡阶段
- addEventListener——兼容:firefox、chrome、IE、safari、opera;
事件委托
原理:事件冒泡
<ul id="ul">
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li>
<li>7</li>
<li>8</li>
</ul>
描述:对于一组元素,我们希望点击对应的元素打印出对应的内容和索引。
思考:你会怎么做呢?这样?
const liList = document.getElementsByTagName("li");
for(let i = 0; i<liList.length; i++){
liList[i].addEventListener('click', function(e){
alert(`内容为${e.target.innerHTML}, 索引为${i}`);
})
}
查找所有的元素,给每个元素添加事件。 思考:这样显然是可以,但是性能可就差了,循环操作dom元素是很耗性能的。
我们应该在ul里面绑定事件:这样!
思考:
- 怎么取内容:e.target.innerHTML;
- 怎么取索引:index = Array.prototype.indexOf.call(liList, target);
const ul = document.querySelector("ul");
ul.addEventListener('click', function (e) {
const target = e.target;
if (target.tagName.toLowerCase() === "li") {
const liList = this.querySelectorAll("li");
index = Array.prototype.indexOf.call(liList, target);
alert(`内容为${target.innerHTML}, 索引为${index}`);
}
})
注意: Array.prototype.indexOf.call(liList, target)相当于liList.indexOf(target)