浏览器的事件模型,就是通过监听函数对事件触发做出反应,事件发生后,浏览器监听到了这个事件,就会执行对应的监听函数,这就是事件驱动编程模式
事件流程
触发一个绑定在元素上的事件后,会经历以下流程:
- 捕获阶段: 事件流程从根元素(body)开始,由外到内进行捕获,直到目标元素(绑定事件的这个元素)
- 目标阶段: 目标阶段就是通过事件捕获,捕捉到了当前事件触发在哪个 DOM 元素上的过程,即目标元素
- 冒泡阶段: 事件流程经过了目标阶段以后,还会从当前目标元素开始,由内到外的一级一级传递到根元素 body 上,类似于气泡浮出水面的过程
上面示意图表达的过程解释如下:
- 捕获:页面上的HTML是一层层嵌套的,当点击其中任意一个元素时,浏览器需要知道,到底是哪个元素被点击了,所以就会从根节点开始一层层往里捕获节点(1——4)
- 目标:浏览器定位到了被点击的元素,成为目标阶段(4——5)
- 冒泡:目标元素开始执行响应事件,但是同理,嵌套着目标元素的父元素同时也被点击了,也要执行它上面绑定的事件(不管有无人为绑定),所以就会从目标元素开始,一层层的往外,最终会到达根节点(5——8)
特定情况下,你只需要目标元素响应事件而要避免它的父元素响应事件时,你就要阻止事件冒泡,使用 stopPropagation() 方法
const element = document.getElementById('app')
element.addEventListener('click', (e) => {
// 阻止当前触发事件冒泡到当前元素的外层元素
e.stopPropagation()
console.log('我被点击了')
})
还有些元素含有默认事件响应(非人为设定的事件响应代码),为了只让目标元素响应事件,你还要阻止默认事件响应,使用 preventDefault()
const element = document.getElementById('app') // 这里 element 是一个 <a> 元素,具有默认的点击后跳转页面的事件
element.addEventListener('click', (e) => {
// 阻止当前触发事件冒泡到当前元素的外层元素
e.preventDefault()
console.log('我将不会进行页面跳转')
})
事件绑定
on
可以直接在 HTML 标签上绑定 Javascript 事件,虽然这种方式非常不推荐使用
<body onload="doSomething()">
<div onclick="console.log('触发事件')">
<div onmousemove="console.log('触发事件')">
在 HTML 标签上绑定事件都是 on + 【事件名】 的形式,例如上面的 onmousemove 。还需要注意两点:
- 标签上的 on 事件监听只能传入 Javascript 语句或者函数调用,而不能仅仅是一个函数名称
<!-- 正确 -->
<body onload="doSomething()">
<!-- 错误 -->
<body onload="doSomething">
- 使用这个方法指定的监听代码,只会在冒泡阶段触发,而且不能控制在哪个阶段来执行
- 同一个元素,只能绑定一个事件监听函数
addEventListener
这个方法是最常用的事件监听注册方式。不同于 HTML 标签上的事件绑定,addEventlistener 可以为同一个元素绑定多个监听函数
const element = document.getElementById('app')
element.addEventListener('click', () => console.log('我被点击了'))
element.addEventListener('mousemove', () => console.log('发生了鼠标移动'))
使用 addEventListener 绑定的事件监听函数必须通过 removeEventListener 来移除
const element = document.getElementById('app')
const callBack = function(){
console.log('do something')
}
element.addEventListener('click', callBack)
// do something ....................
// 移除事件监听
element.removeEventListener('click', callBack)
addEventListener 的标准形式是 element.addEventListener(type, listener, options),它一共有三个参数:
- type : 触发的事件类型,比如 click、mouserover等等
- listener: 事件监听所绑定的回调函数,也就是事件被触发后会执行的操作
- options: options 是一个对象,它有三个属性:
- capture: 布尔值,指定 listener 是否在事件的捕获阶段触发,默认值为
false - once: 布尔值,表示 listener 在添加之后最多只调用一次。如果为 true, listener 会在调用之后自动移除
- passive: 布尔值,设置为true时,表示 listener 永远不会调用
preventDefault()
- capture: 布尔值,指定 listener 是否在事件的捕获阶段触发,默认值为
第三个参数还有一种写法,那就是只传递一个布尔值 useCapture ,它跟 options.capture 的作用相同,不过,这是过去的写法,不建议使用
下面来详细说一下,options.capture 这个参数的作用,首先有一段程序:
// 样式代码忽略....
<div id="out">
<h1>out</h1>
<div id="middle">
<h1>middle</h1>
<div id="inside">
<h1>inside</h1>
<button id="button">点击我</button>
</div>
</div>
</div>
<script>
const out = document.getElementById('out');
const middle = document.getElementById('middle');
const inside = document.getElementById('inside');
const button = document.getElementById('button');
out.addEventListener('click', () => {console.log("out")}, {
capture: false
});
inside.addEventListener('click', () => {console.log("inside")}, {
capture: false
});
middle.addEventListener('click', () => {console.log("middle")}, {
capture: false
});
button.addEventListener('click', () => {console.log("button")}, {
capture: false
});
</script>
前面说过,capture 这个参数默认为 false,首先来看看事件触发的顺序会是怎样:
结合上面的事件流程的图就可以知道,所有的事件都是在冒泡阶段被触发的,从 button 开始一直向外冒泡,触发了所有外层元素的事件
现在把所有的 capture 都设置为 true,再来看看结果会是怎样
现在,顺序完全反了过来,从最外层的 out 开始一直往内触发事件,这不就是事件捕获嘛!所以,capture 的参数的作用应该是明了的:指定事件触发的顺序,也就是指定事件是捕获阶段触发,还是在冒泡阶段触发
事件委托
事件委托这个概念就相对简单一些,也是基于上面的事件模型,我们用一个程序来举例子。现在有一个 ul 无序列表,里面有很多项 li ,需要你实现的是:点击每个 li 之后,在控制台打印这个 li 的文本内容
<ul>
<li>this is li <h1>1</h1></li>
<li>this is li <h1>2</h1></li>
<li>this is li <h1>3</h1></li>
<li>this is li <h1>4</h1></li>
<li>this is li <h1>5</h1></li>
</ul>
<script type="text/javascript">
const allLi = document.querySelectorAll('li');
allLi.forEach(li => {
li.addEventListener('click',show);
})
function show(e){
console.log(e.currentTarget.innerText);
}
</script>
上面的代码就可以实现这个需求了,但是,现在仔细想一想,问自己几个问题:
ul和li是不是嵌套的关系?li上绑定的事件是不是默认会冒泡到ul上?
上面两个问题的答案是肯定的,所以,你发现了上面代码的问题所在了吗——通过 forEach 循环为每个 li 元素都设置了事件监听,但其实这非常没有必要,反正最后 li 上绑定的事件都会默认冒泡到 ul 上,所以,我们直接在 ul 上设定一次事件绑定不就行了?!只要点击了 li ,ul 上的事件监听就会被触发:
const ul = document.querySelector('ul');
ul.addEventListener('click', show);
function show(e) {
console.log(e.target.innerText);
}
现在,只要点击 li ,show 方法就会触发,通过 event.target 就能获取当前点击的元素是哪个。
这就是事件委托,它的意思是:不必给每个子元素都绑定一遍事件监听,只需要给它们的父元素绑定一次,然后通过事件冒泡来触发事件就可以了
事件目标
当我们通过点击或者是别的方式,来触发一个元素的事件监听时,我们往往需要获取到所触发的这个元素的 DOM节点对象 ——事件目标。一共有三个属性可以获取到事件目标对象,但它们之间稍微有些不同
event.target
上面实现事件委托依靠的就是 event.target ,它表示的是 事件触发的元素。
还是引用上面的例子:当我们点击 li 的时候,event.target 就是 li ,但如果我们点击的是 h1 ,那么 event.target 就是 h1 ,所以,在事件触发的时候,你点击的元素是谁,event.target 就是谁(换成别的任何事件类型同样适用)
event.currentTarget
它指的是:事件绑定的元素,而 event.target 则总是事件触发的元素。如何区分?在上面的例子中,事件 show 是绑定在 ul 上的,所以有如下推断:当我们点击 li 的时候
event.currentTarget表示的是ulevent.target表示的是当前所点击的元素,也就是li
我们改一下之前的代码,然后点击 li,再看看打印结果,检验我们的推断是否正确:
const ul = document.querySelector('ul');
ul.addEventListener('click', show);
function show(e) {
console.log('target: ', e.target); // 事件触发的 dom 元素
console.log('currentTarget: ', e.currentTarget); // 事件绑定的 dom 元素
}
最后,还有个地方需要注意一下,浏览器还实现了一个叫 event.srcElement 的属性,它的作用跟 event.target 是一样的,但是 event.srcElement 仅仅是为了支持 IE 浏览器而开发的属性,所以,你在开发的时候如果不需要适配 IE ,那么你使用 event.target 就好了。珍爱生命,远离 IE!
如果你没看懂,那么下面是参考资料,这样你就可以直接从源头处开始,自己研究:
- event.target MDN文档
- event.currentTarget MDN 文档
- 详解addEventListener的三个参数之useCapture 张生荣
- 事件模型-Javascript教程 阮一峰老师