事件详解
1 事件注册
给元素添加事件就称之为注册事件,注册事件也叫绑定事件。注册事件有两种方式:
- 传统注册方式
- 监听注册方式
1.1 传统注册
传统注册方式是通过为指定事件源绑定回调函数的形式来处理事件,当指定事件触发以后回调函数就会被调用,这样就可以实现页面和用户之间的交互了。
可以通过以下两种方式为一个元素绑定事件处理程序:
1)通过HTML元素指定事件属性来绑定
<button onclick="console.log('haha,骗你的。。。')">点我有奖</button>
2)通过DOM对象指定的属性来绑定
<button id="btn">点我。。</button>
<script>
var btn = document.querySelector('#btn')
btn.onclick = function () {
console.log('谢谢。。。。')
};
</script>
1.2 监听注册
另一种方式比较特殊事件注册方式是通过事件监听来注册,称为设置事件监听器,它是 W3C 标准推荐使用的方式。通过 EventTarget.addEventListener() 方法来实现。
EventTarget.addEventListener() 将指定的监听器注册到 eventTarget 上,当该对象触发指定的事件时,指定的回调函数就会被执行。 事件目标可以是一个文档上的元素 Element,Document 和 Window 或者任何其他支持事件的对象 (比如 XMLHttpRequest)。
addEventListener() 的工作原理是将实现 EventListener 的函数或对象添加到调用它的 EventTarget 上的指定事件类型的事件侦听器列表中。
语法格式:
target.addEventListener(type, listener, useCapture);
参数说明:
- type:必须,事件监听的类型,如 click、mouseover
- listener:必须,一个实现了 EventListener 接口的对象,或者是一个函数
- useCapture:可选,表示是冒泡还是捕获。值为 true 表示捕获,否则表示冒泡
示例:
<button>点击</button>
<script>
var btn = document.querySelector('button')
btn.addEventListener('click', function () {
console.log('大家周末好!');
});
</script>
1.3 两种注册比较
1.如果使用传统方式进行注册,当对同一个对象添加相同事件时,后面的事件会覆盖掉前面的事件;如果使用监听方式进行注册,当同一个对象绑定相同多个事件时,这些事件都会被执行。
2.传统注册方式只能进行冒泡,不能进行捕获;而监听注册方式可以冒泡也可以捕获。
<button class="btn1">传统注册</button>
<button class="btn2">监听注册</button>
<script>
var btn1 = document.querySelector('.btn1');
var btn2 = document.querySelector('.btn2');
// 传统注册
btn1.onclick = function () {
console.log('明天星期一,我们休息,大家可以睡个懒觉,哈哈');
};
btn1.onclick = function () {
console.log('星期二我们继续');
};
// 监听注册
btn2.addEventListener('click', function () {
console.log('今天星期天,快下课了,哈哈');
});
btn2.addEventListener('click', function () {
console.log('但是还得花时间来消化今天的内容。');
});
</script>
2 解绑事件
对于传统注册方式,解绑事件的语法为:
eventTarget.事件类型 = null;
对于监听注册方式,解绑事件的语法为:
eventTarget.removeEventListener(type, listener[, useCapture]);
示例:
<button class="btn1">传统注册</button>
<button class="btn2">监听注册</button>
<script>
var btn1 = document.querySelector('.btn1');
var btn2 = document.querySelector('.btn2');
// 传统注册
btn1.onclick = function () {
console.log('明天星期一,我们休息,大家可以睡个懒觉,哈哈');
};
// 解绑事件
btn1.onclick = null;
// 监听注册
function fn() {
console.log('今天星期天,快下课了,哈哈');
btn2.removeEventListener('click', fn);
}
/*
btn2.addEventListener('click', function () {
console.log('今天星期天,快下课了,哈哈');
});
btn2.removeEventListener('click', function () {
console.log('我们没有关系了。')
});*/
btn2.addEventListener('click', fn);
</script>
注意:
- 如果把解绑语句放到注册语句外,那么注册的事件将无效。如果放到注册事件代码块中,那么会执行一次。
- 对于监听注册方式的解绑,需要注意事件类型和监听函数必须是相同的。
3 事件流
由于在 HTML 中的标签都是相互嵌套的,我们可以将元素想象成一个盒子装一个盒子,document 是最外面的大盒子。当你单击一个 div 时,同时你也单击了div 的父元素,甚至整个页面。
那么是先执行父元素的单击事件,还是先执行div的单击事件 ?
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>事件流</title>
<style>
.outer {
overflow: hidden;
width: 300px;
height: 300px;
margin: 100px auto;
background-color: blueviolet;
text-align: center;
}
.inner {
width: 200px;
height: 200px;
margin: 50px;
background-color: cornflowerblue;
line-height: 200px;
color: #fff;
}
</style>
</head>
<body>
<div class="outer">
<div class="inner">内部盒子</div>
</div>
<script>
var inner = document.querySelector('.inner');
var outer = document.querySelector('.outer');
inner.addEventListener('click', function () {
console.log('inner');
});
outer.addEventListener('click', function () {
console.log('outer');
});
// 给 body 添加点击事件
document.body.addEventListener('click', function () {
console.log('body');
});
// 给 html 添加点击事件
document.documentElement.addEventListener('click', function () {
console.log('html');
});
// 给 document 添加点击事件
document.addEventListener('click', function () {
console.log('document');
});
window.addEventListener('click', function () {
console.log('window');
});
</script>
</body>
</html>
事件流又称为事件传播,描述的是从页面接收事件的顺序。当事件发生时会在元素节点之间按照特定的顺序传播,这个传播过程就叫DOM事件流。
事件流包括三个阶段:事件捕获阶段(capture phase)、处于目标阶段(target phase)和事件冒泡阶段(bubbling phase)。
- 捕获阶段:从不确定事件源到确定事件源依次向下响应。
- 目标阶段:真正的目标节点正在处理事件的阶段(比如你点击鼠标触发事件的那个节点)。
- 冒泡阶段:从明确事件源到不明确的事件源依次向上响应。
小故事:IE(微软公司)提出从目标元素开始向外逐层响应事件,也就是冒泡型事件流;而Netscape(网景公司)提出从层外向目标元素响应事件,也就是捕获型事件流。最终由 W3C 组织根据折中的方式来制定了统一标准——先捕获再冒泡。
注意:
- JavaScript 代码中只能执行捕获或冒泡其中一个阶段,要么冒泡,要么捕获。
- onclick 与 attachEvent 方式注册的事件只有冒泡阶段。
- addEventListener(type, listener[, useCapture]) 第三个参数为 true 表示捕获阶段,默认空着或者 false 为冒泡阶段。
- 有些事件不支持冒泡阶段(常见的几个):blur、focus、mouseenter、mouseleave、load、unload、resize。
3.1 事件冒泡
事件冒泡是从明确的对象向不明确对象传播的事件。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>事件冒泡</title>
<style>
.box {
width: 200px;
height: 200px;
background-color: red;
}
</style>
</head>
<body>
<div class="box"></div>
<script>
var box = document.querySelector('.box')
box.addEventListener('click', function () {
console.log("div")
});
document.body.addEventListener('click', function () {
console.log('body');
});
document.documentElement.addEventListener('click', function () {
console.log('html');
});
</script>
</body>
</html>
事件冒泡是从目标对象向上传播。
3.2 事件捕获
事件捕获是从不明确对象向目标对象的捕获的过程。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>事件捕获</title>
<style>
.box {
width: 200px;
height: 200px;
background-color: red;
}
</style>
</head>
<body>
<div class="box"></div>
<script>
var box = document.querySelector('.box')
box.addEventListener('click', function () {
console.log("div")
},true);
document.body.addEventListener('click', function () {
console.log('body');
},true);
document.documentElement.addEventListener('click', function () {
console.log('html');
},true);
</script>
</body>
</html>
注意:
- 要想进行捕获,只能使用事件监听注册方式,不能使用事件传统注册方式
- 需要把监听注册方法的第三个参数设置为 true
对于今后的开发来说,我们通常都只会处理冒泡,而不会去处理捕获。
4 事件对象
在触发 DOM 上的某个事件时,会产生一个事件对象 event。这个对象中包含着所有与事件有关的信息。包括导致事件的元素,事件的类型以及其他与特定事件相关的信息。
比如:
鼠标操作导致的事件对象中会包含鼠标位置的信息。
键盘操作导致的事件对象中会包含按下的键有关的信息。
事件发生,系统会自动生成一个事件对象。
获取事件对象是通过一个形参来操作的,这个形参可以自己确定,一般用event、evt、e表示,形参写在函数的括号内。
注意:
① 事件对象只能在处理函数内部访问,处理函数允许结束后该对象自动销毁。
② 有兼容问题
示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>事件对象</title>
</head>
<body>
<ul>
<li>1</li>
<li>2</li>
</ul>
<script>
var ul = document.querySelector('ul');
ul.addEventListener('click', function (event) {
// 处理兼容性问题
event = event || window.event;
console.log(event);
// 从事件对象 event 中获取目标对象
console.log(event.target.innerHTML);
});
</script>
</body>
</html>
5.3 事件属性
事件对象常用的属性和方法:
示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>事件对象属性</title>
<style>
.box {
width: 100px;
height: 100px;
background-color: #ff6700;
}
</style>
</head>
<body>
<div class="box"></div>
<script>
var box = document.querySelector('.box')
box.onclick = function (event) {
event = event || window.event;
// 1. 获取目标对象
// 1.1 target 标准
console.log(event.target)
// 1.2 srcElement 非标准
console.log(event.srcElement);
// 2. 获取事件类型,不带 on
console.log(event.type);
// 3. 获取在可视化区域的坐标
console.log(event.clientX + ', ' + event.clientY);
};
</script>
</body>
</html>
5.1 事件目标
目标对象有 currentTarget、target 和 srcElement。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>事件对象目标</title>
<style>
ul,li {
list-style: none;
}
.item {
width: 100px;
height: 50px;
background-color: #ff6700;
margin: 10px;
text-align: center;
line-height: 50px;
color: white;
}
</style>
</head>
<body>
<ul id="box">
<li class="item">1</li>
<li class="item">2</li>
</ul>
<script>
var box = document.querySelector('#box')
box.onclick = function (event) {
event = event || window.event;
console.log(event.target);
console.log(event.srcElement);
console.log(event.currentTarget);
console.log(this);
};
</script>
</body>
</html>
说明:
- currentTarget:返回的是绑定事件的对象
- target:返回的是事件的实际目标对象(IE8不支持)
- srcElement:返回的是事件的实际目标对象(IE8支持)
e.target 和 this 的区别:
- this 是事件的绑定的元素,它与 currentTarget 是指向同一个
- e.target 是事件触发的元素
5.2 事件委托
事件委托指将事件统一绑定给元素的共同的祖先元素,当后代元素上的事件触发时,会一直冒泡到祖先元素,从而通过祖先元素的响应函数来处理事件。
事件委托就是利用冒泡,通过委托可以减少事件绑定的次数,提高程序的性能。
下面的示例用来演示事件委托的好处。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>事件委托</title>
<style>
* {
padding: 0;
margin: 0;
}
ul {
list-style: none;
overflow: hidden;
margin-top: 80px;
}
ul li {
float: left;
width: 100px;
height: 30px;
text-align: center;
line-height: 30px;
color: #fff;
background-color: #000;
margin: 0 10px
}
</style>
</head>
<body>
<ul id="box">
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>
<script>
/*
var lis = document.querySelectorAll('li')
for (var i = 0; i < lis.length; i++) {
lis[i].onmouseover = function () {
this.style.backgroundColor = 'blue';
};
lis[i].onmouseout = function () {
this.style.backgroundColor = 'black';
};
}*/
var ul = document.querySelector('#box')
ul.onmouseover = function (e) {
if (e.target.nodeName.toLowerCase() === 'li') {
e.target.style.backgroundColor = 'blue';
}
};
ul.onmouseout = function (e) {
if (e.target.nodeName.toLowerCase() === 'li') {
e.target.style.backgroundColor = 'black';
}
};
</script>
</body>
</html>
从上面的代码可以发现,当把事件绑定在 ul 上时,减少了一个循环。这样就可以提高程序性能。而把事件绑定在 ul 上的方式就是事件委托。
事件委托的第二个好处:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>事件委托</title>
<style>
* {
padding: 0;
margin: 0;
}
ul {
list-style: none;
overflow: hidden;
margin-top: 80px;
}
ul li {
float: left;
width: 100px;
height: 30px;
text-align: center;
line-height: 30px;
color: #fff;
background-color: #000;
margin: 0 10px
}
</style>
</head>
<body>
<ul id="box">
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>
<script>
var ul = document.querySelector('#box')
/*
var lis = document.querySelectorAll('li')
for (var i = 0; i < lis.length; i++) {
lis[i].onmouseover = function () {
this.style.backgroundColor = 'blue';
};
lis[i].onmouseout = function () {
this.style.backgroundColor = 'black';
};
}*/
ul.onmouseover = function (e) {
if (e.target.nodeName.toLowerCase() === 'li') {
e.target.style.backgroundColor = 'blue';
}
};
ul.onmouseout = function (e) {
if (e.target.nodeName.toLowerCase() === 'li') {
e.target.style.backgroundColor = 'black';
}
};
var li = document.createElement('li');
li.innerHTML = '6';
ul.append(li);
</script>
</body>
</html>
使用事件委托的第二个好处是后续添加的子元素也可以拥有委托事件。
6 事件方法
6.1 阻止事件冒泡
所谓的冒泡指的就是事件的向上传播,当后代元素上的事件被触发时,其祖先元素的相同事件也会被触发。事件冒泡的这一特性有好处,也有坏处。如果不希望发生事件冒泡可以通过事件对象来取消冒泡。
示例:取消冒泡
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>阻止事件冒泡</title>
<style>
.outer {
overflow: hidden;
width: 300px;
height: 300px;
margin: 100px auto;
background-color: blueviolet;
text-align: center;
}
.inner {
width: 200px;
height: 200px;
margin: 50px;
background-color: cornflowerblue;
line-height: 200px;
color: #fff;
}
</style>
</head>
<body>
<div class="outer">
<div class="inner">内部盒子</div>
</div>
<script>
var inner = document.querySelector('.inner');
var outer = document.querySelector('.outer');
// 当点击 inner 时,不要触发外部的 div 的点击事件
inner.onclick = function (event) {
console.log('inner')
// 处理兼容性问题
if (event.stopPropagation()) {
event.stopPropagation(); // 这个方法早期IE版本不支持
} else {
event.cancelBubble = true; // IE支持的
}
};
outer.addEventListener('click', function (event) {
console.log('outer');
});
</script>
</body>
</html>
说明:如果不希望冒泡的发,我们可以使用事件对象中的 stopPropagation() 方法来阻止事件冒泡的发生。
在事件对象中除了 stopPropagation() 方法可以阻止事件冒泡外,还有一个方法叫 stopImmediatePropagation() ,它也可以阻止事件冒泡。
这两方法有什么区别?
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>两个阻止事件冒泡方法的区别</title>
<style>
.outer {
overflow: hidden;
width: 300px;
height: 300px;
margin: 100px auto;
background-color: blueviolet;
text-align: center;
position: relative;
}
.inner {
width: 200px;
height: 200px;
margin: 50px;
background-color: cornflowerblue;
line-height: 200px;
color: #fff;
}
</style>
</head>
<body>
<div class="outer">
<div class="inner">内部盒子</div>
</div>
<script>
var inner = document.querySelector('.inner');
var outer = document.querySelector('.outer');
inner.addEventListener('click', function (event) {
console.log('inner');
//event.stopPropagation();
event.stopImmediatePropagation();
});
inner.addEventListener('click', function () {
console.log('再次点击')
});
outer.addEventListener('click', function (event) {
console.log('outer');
});
</script>
</body>
</html>
相同点:都能阻止事件冒泡的发生。
不同点:
- stopPropagation() 方法只能阻止事件冒泡的发生,不能阻止同一个对象的相同事件的执行。
- stopImmediatePropagation() 方法既可以阻止事件冒泡的发生,也可以阻止同一个对象的相同事件的执行。
6.2 阻止默认行为
在 HTML 中很多元素都具有默认行为,例如超链接具有点击后跳转页面的默认行为,提交按钮具有提高表单的默认行为。如何来阻止这个默认行为呢?
可以通过事件对象中的方法来进行阻止。
<a href="javascript:void(0)">淘宝</a>
<a href="javascript:;">腾讯</a>
<a href="https://www.baidu.com">百度</a>
<script>
var as = document.querySelectorAll('a');
as[2].onclick = function (event) {
//console.log(event);
event.preventDefault();
};
</script>
说明:对于超链接,我们可以有以上三种方式来阻止链接的默认行为。
对于阻止提交按钮提交表单的默认行为:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>阻止事件默认行为</title>
</head>
<body>
<p>
<form action="https://www.baidu.com" method="post">
<input type="text" name="s">
<input type="submit" value="提供">
</form>
</p>
<script>
var form = document.querySelector('form');
form.addEventListener('submit', function (event) {
event.preventDefault();
});
</script>
</body>
</html>
7 鼠标事件
event 事件对象是事件相关的一系列信息的集合,它可分为鼠标事件对象 MouseEvent 和键盘事件对象 KeyboardEvent。
鼠标事件 | 说明 |
e.clientX | 返回鼠标相对于浏览器窗口可视区的 X 坐标 |
e.clientY | 返回鼠标相对于浏览器窗口可视区的 Y 坐标 |
e.pageX | 返回鼠标相对于文档页面的 X 坐标 |
e.pageY | 返回鼠标相对于文档页面的 Y 坐标 |
e.screenX | 返回鼠标相对于电脑屏幕的 X 坐标 |
e.screenY | 返回鼠标相对于电脑屏幕的 Y 坐标 |
示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>鼠标事件</title>
<style>
*{
padding: 0;
margin: 0;
}
body {
height: 2000px;
}
.box {
position: fixed;
top: 0;
left: 0;
width: 300px;
height: 300px;
margin: 50px;
background-color: #ff6700;
}
</style>
</head>
<body>
<div class="box"></div>
<script>
var box = document.querySelector('.box')
box.addEventListener('click', function (event) {
event = event || window.event;
// client 是针对于可视化区域的
console.log(event.clientX, event.clientY);
// page 是针对于页面
console.log(event.pageX, event.pageY)
// screen 是针对于电脑屏幕
console.log(event.screenX, event.screenY);
});
</script>
</body>
</html>
7.1 放大镜案例
案例效果:
案例分析过程:
1.计算鼠标在small_box 中的坐标
2.计算鼠标在 small_box 中的可移动的范围(边界)
3.计算大图可移动的坐标,需要理解这个公式
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>放大镜效果</title>
<style>
* {
padding: 0;
margin: 0;
}
#box {
width: 430px;
height: 430px;
border: 1px solid #DDDDDD;
position: relative;
margin: 50px;
}
#small_box {
width: 430px;
height: 430px;
position: relative;
}
#small_box #mask {
position: absolute;
width: 210px;
height: 210px;
background: url(images/dotted.png) repeat;
top: 0;
left: 0;
display: none;
}
#small_box #mask:hover {
cursor: move;
}
#big_box {
position: absolute;
left: 440px;
top: 0;
width: 430px;
height: 430px;
border: 1px solid #ddd;
overflow: hidden;
display: none;
}
#big_box img {
position: absolute;
z-index: 5;
}
</style>
</head>
<body>
<div id="box">
<div id="small_box">
<img src="images/photo.jpg" alt="">
<span id="mask">
</span>
</div>
<div id="big_box">
<img src="images/photo01.jpg">
</div>
</div>
<script>
window.onload = function () {
// 1. 获取元素
var box = document.querySelector('#box');
var smallBox = box.children[0];
var bigBox = box.children[1];
var smallImg = smallBox.children[0];
var mask = smallBox.children[1];
var bigImg = bigBox.children[0];
// 事件委托
smallBox.onmouseover = function () {
// 1. 让遮罩层和大盒子显示
bigBox.style.display = 'block';
mask.style.display = 'block';
// 2. 让遮罩层可以移动
smallBox.addEventListener('mousemove', function (e) {
// 2.1 计算遮罩层移动的 x 坐标和 y 坐标的值
var moveX = e.clientX - smallBox.offsetLeft - box.offsetLeft - mask.offsetWidth * 0.5;
var moveY = e.clientY - smallBox.offsetTop - box.offsetTop - mask.offsetHeight * 0.5;
// 2.2 处理遮罩层边界
if (moveX < 0) {
moveX = 0;
} else if (moveX >= (smallBox.offsetWidth - mask.offsetWidth)) {
moveX = smallBox.offsetWidth - mask.offsetWidth;
}
if (moveY < 0) {
moveY = 0;
} else if (moveY >= (smallBox.offsetHeight - mask.offsetHeight)) {
moveY = smallBox.offsetHeight - mask.offsetHeight;
}
// 2.3 让遮罩层进行移动
mask.style.left = moveX + 'px';
mask.style.top = moveY + 'px';
// 3. 让大图进行移动(注意大图的方向和左边的移动方向相反)
// 大图移动的计算公式
// moveX / 大图X方向移动的位置 = (小图的宽度 - 遮罩层的宽度) / (大图的宽度 - 大图所在盒子的宽度)
// 计算演变过程
// moveX / x = (smallBox.offsetWidth - mask.offsetWidth) / (bigImg.offsetWidth - bigBox.offsetWidth)
// moveX = (smallBox.offsetWidth - mask.offsetWidth) / (bigImg.offsetWidth - bigBox.offsetWidth) * x
// x = moveX / (smallBox.offsetWidth - mask.offsetWidth) * (bigImg.offsetWidth - bigBox.offsetWidth)
// 3.1 计算大图的移动坐标
var x = moveX / (smallBox.offsetWidth - mask.offsetWidth) * (bigImg.offsetWidth - bigBox.offsetWidth);
var y = moveY / (smallBox.offsetHeight - mask.offsetHeight) * (bigImg.offsetHeight - bigBox.offsetHeight);
// 3.2 给大图设置坐标
bigImg.style.left = -1 * x + 'px';
bigImg.style.top = -1 * y + 'px';
});
};
// 鼠标移开
smallBox.addEventListener('mouseout', function () {
mask.style.display = 'none';
bigBox.style.display = 'none';
});
};
</script>
</body>
</html>