JavaScript与HTML交互是通过事件实现的,事件代表文档或浏览器窗口中某个有意义的时刻。它是元素天生自带的一种行为,当我们操作元素时,也会把对应的事件触发。而事件绑定是给元素的某个行为绑定一个方法,目的是当事件行为触发时,可以处理一些操作。
常用的事件行为有以下几种:鼠标事件 ,键盘事件,资源事件,焦点事件,触摸事件等等,在了解一些常用事件前我们要先了解事件的传播机制。
一、事件传播机制
事件流又称为事件传播,它描述了页面接收事件的顺序。DOM2级事件规定事件流包括三个阶段:捕获阶段(capture phase)、目标阶段(target phase)冒泡阶段(bubbling phase)。
捕获阶段:在事件触发时,会先执行一个阶段叫做“捕获阶段”,从最外层向最里层事件源依次进行查找(从外到内)。可以看出捕获阶段的目是为冒泡阶段事先计算好传播的层级路径。
目标阶段:当前元素的相关事件行为触发,如果绑定了方法,则把方法执行。
冒泡阶段:触发当前元素的某一个事件行为,不仅仅它这个行为被触发,而且它所有的的祖先元素相关事件行为都会被依次触发(从内到外的顺序)。
首先发生的是事件捕获,为获取事件提供机会。然后是实际目标接收到事件,最后一个阶段是冒泡阶段,最迟要在这个阶段对事件做出响应。
不论是捕获阶段还是冒泡阶段,在寻找目标元素过程和向外返回的程中,所遇到每一个元素身上如果有相同事件的事件处理函数都会被调用。
DOM0事件绑定,绑定的方法都是控制在目标或者是冒泡阶段执行,DOM2可以控制在捕获阶段执行,最后参数true就是控制在捕获阶段执行,只不过一般不用。
<!DOCTYPE html>
<head>
<title>Document</title>
</head>
<body>
<div class="outer">
<div class="inner">
<div class="center">执行</div>
</div>
</div>
</body>
</html>
复制代码
由上面的结构,可以出事件捕获是从:window ->document->html->body->outer_div->inner_div->center_div
目标阶段:center_div
冒泡阶段:center_div->inner_div_outer_div->body->html->document->window
用下图来作为说明:
二、事件处理程序
事件意味着用户或浏览器执行的某种动作,像是双击或者是加载等,为响应事件而调用的函数被称为事件处理程序(事件监听器)。事件处理程序的名字以“on”开头,如onclick、onload。
特定元素支持的每个事件都可以使用事件处理程序的名字以html属性的形式来指定。这时属性值必须是能够执行的js代码。如下:
<input type="button" value="click me" onclick="console.log('clicked')">
复制代码
要注意属性值是js代码,所以不能在未经转义的情况下使用html语法字符。
在html中定义的事件处理程序可以包含精确动作指令,也可以调用在页面其他地方定义的脚本。如下:
<input type="button" value="click me" onclick="fn()">
<script>
function fn() {
console.log('clicked!');
console.log(event)
}
</script>
复制代码
以这种方式指定的事件处理程序首先会创建一个函数来封装属性的值,这个函数有一个特殊的局部变量event,其中保存的就是event对象。
2.1 DOM0级事件处理程序
事件绑定:给元素的某个事件行为绑定方法,这样事件行为触发的时候,对应绑定的方法会执行,完成一些需要完成的功能。
- 元素.on事件行为 = function(){};
- 原理:事件绑定的原理就是给当前元素对象的某些私有属性赋值一个函数,当事件行为触发,浏览器会帮助把绑定的方法执行。
- 注意:on只能给元素添加一个事件,如果说有多个on,那后面的会把前面的进行覆盖。
- 不足:如果元素私有属性中不具备某个事件的私有属性则无法给这些事件绑定方法
- 事件移除:元素.on事件行为 = null;
<button id="btn">click me</button>
<script>
const btn = document.getElementById('btn');
btn.onclick = function () {
//可以通过this访问元素的任何属性和方法
console.log(this.id); //btn
}
btn.onclick = null;//移除事件处理程序
</script>
复制代码
2.2 DOM2级事件处理程序
【标准浏览器】
- 元素.addEventListener(事件行为,function(){},true/false);
- 参数1:事件行为,不加on,它是一个字符串;
- 参数2:事件绑定,这个事件绑定函数可以是匿名也可以是命名,最好命名;
- 参数3:布尔值:true代表事件是在捕获阶段发生,falses代表事件在冒泡阶段发生。
- 原理:基于元素的原型链找到EventTarget.prototype,使用内置的方法进行事件绑定和移除。它是基于浏览器内置的事件池机制完成事件监听和方法绑定。
- 注意:可以给当前元素的某个事件类型绑定多个不同的方法,事件触发会按照绑定的顺序把方法依次执行。 基于addEventListener向事件池增加方法,存在去重的机制“同一个元素,同一个事件类型,在事件池中中只能存储一遍这个方法,不能重复的存储”。
- 事件移除:元素.removeEventListener(事件行为,function(){},true/false);需要注意不能移除addEventListener添加的匿名函数。只能称除同一阶段的绑定函数(布尔值相同)。
大多数情况下,事件处理程序会被添加到事件流的冒泡阶段,这样跨浏览器兼容性好。
<button id="btn">click me</button>
<script>
let btn = document.getElementById("btn");
btn.addEventListener("click", () => {
console.log(this.id);
}, false);
btn.addEventListener('click', () => {
console.log('hello world!')
}, false);
</script>
复制代码
【IE6-8】
- 元素.attachEvent(’onxxxx‘,function(){});
- 事件移除:detachEvent(event,function(){});
因为IE8及更早版本只支持事件冒泡,所以使用
attachEvent()添加的事件处理程序会添加到冒泡阶段。
三、事件对象
在触发DOM上某个事件时,会产生一个事件对象event,这个对象中包含着所有与事件有关的信息。比如导致事件的元素,发生的事件类型等等。所有浏览器都支持event对象,尽管支持的方式不同。事件对象包含与特定事件相关的属性和方法,不同事件生成的事件对象也会包含不同的属性和方法,不过所有事件对象都包含以下常用公共属性和方法。
属性/方法 | 类型 | 说明 |
---|---|---|
currentTarget | 元素 | 当前事件处理程序所在的元素 |
target | 元素 | 事件目标 |
type | 字符串 | 被触发的事件类型 |
preventDefault() | 函数 | 用于取消事件的默认行为 |
defaultPrevented | 布尔值 | true表示已经调用了preventDefault()方法 |
stopPropagation()方法 | 函数 | 用于取消所有后续事件捕获或事件冒泡 |
在事件处理程序内部,this对象始终等于currentTarget的值,而target只包含事件的实际目标,它们二者当事件处理程序直接添加在了意图的目标上时相等。
<button id="btn">click me</button>
<script>
var btn = document.getElementById("btn");
btn.onclick = function (event) {
console.log(event.currentTarget === event.target);//trueF
}
</script>
复制代码
事件对象和函数以及给谁绑定的事件没有必然的关系,它存储的是当前本次操作的相关信息,操作一次只能有一份信息,第二次存储的信息会把上一次操作存储的信息替换。
preventDefault()方法用于阻止特定事件的默认动作。stopPropagation()方法用于立即阻止事件流在DOM结构中传播,取消后续的事件捕获或冒泡。
四、事件分类
4.1 用户界面事件
针对window对象触发的事件,应用在<body>标签上
事件名称 | 描述 |
---|---|
load | 当页面加载完成后触发 |
unload | 当页面完全被卸载后触发 |
resize | 当窗口大小被改变时触发 |
scroll | 当用户滚动窗口或滚动其他任何可滚动的元素时触发该事件 |
select | 在文本框上当用户选择一个或多个字符时触发 |
大多数html事件与window对象和表单控件有关。
4.2 焦点事件
事件名称 | 描述 |
---|---|
blur | 当元素失去焦点时触发 |
focus | 当元素获得焦点时触发 |
4.3 鼠标和滚轮事件
鼠标事件一共有11类,如下所示:
事件名称 | 描述 |
---|---|
click | 点击事件 |
dbclick | 双击事件 |
mousedown | 鼠标按下 |
mouseup | 鼠标抬起 |
mousemove | 鼠标移动(指针在元素内移动持续触发) |
mouseover | 用户把鼠标光标从元素外部移到元素内部时触发 |
mouseenter | 用户把鼠标光标从元素外部移到元素内部时触发。不冒泡,也不会在光标经过后代元素时触发 |
mouseout | 鼠标离开(指南移动元素,或者是移到它的子元素上) |
mouseleave | 用户把鼠标光标从元素内部移到元素外部时触发。这个事件不冒泡,也不会在光标经过后代元素时触发 |
wheel | 滚轮向任意方向滚动 |
contextmenu | 鼠标右键点击 |
页面中的所有元素都支持鼠标事件。除了 mouseenter 和 mouseleave,所有鼠标事件都会冒泡,
都可以被取消,而这会影响浏览器的默认行为。
当鼠标移入时,会依次的触发mousemove事件和mouseenter事件,再触发mousemove事件;
当鼠标移出时,触发mousemove,mouseout,mouseleave事件;
双击鼠标时,触发mousedown,mouseup,click,mousedown,mouseup,click,dbclick事件;
点击鼠标右键时,触发mousedown,mouseup,contextmenu事件。
嵌套元素的移入移出时:
-
从父级元素进入子级元素时,顺序为:父级元素的mouseout、子级元素的mouseover、父级元素的mouseover、子级元素的mouseenter
-
从子级元素进入父级元素时,顺序为:子级元素的mouseout、父级元素的mouseout、子级元素的mouseleave、父级元素的mouseover。
mouseout和mouseover的区别:
-
mouseover本身不是进入,而是看鼠标在哪个元素上面,从子元素进入父元素,触发父元素的mouseover,从父元素进入子元素触发父元素的mouseout。
-
mouseleave默认阻止了事件的冒泡传播,从父元素进入子元素,从子元素重新进入父元素,父元素的mouseenter和mouseleave都不针对触发。
-
所以盒子中如果有后代元素,我们尽量用mouseenter,但是需要冒泡传播做的事情,还是需要用mouseover。
<style>
.outer{
margin:200px auto;width: 400px;height: 400px;background-color: lightgreen;}
.inner{
margin:200px auto;width: 300px;height: 300px;background-color: lightblue;
}
.center{margin:200px auto;width: 200px;height: 200px;background-color: lightpink;
}
</style>
<body>
<div class="outer">
<div class="inner">
<div class="center"></div>
</div>
</div>
</body>
复制代码
鼠标事件在DOM3Events中对应的类型是“MouseEvent”。鼠标事件对象-> MouseEvent.prototype->UIEvent.prototype->Event.prototype->Object.prototype
常用的属性和方法有:
- clientX/clientY: 当前鼠标触发点距离当前窗口左上角的x/y轴坐标(客户端坐标);
<div id="box"></div>
<script>
let div = document.getElementById("box");
div.addEventListener("click", (event) => {
console.log(`Client coordinates: ${event.clientX}, ${event.clientY}`);
});
</script>
复制代码
通过以上例子,当元素被点击时,会显示事件发生时鼠标光标在客户端视口中的坐标。客户端坐标不考虑页面滚动,这两个值也不代表鼠标在页面上的位置。
- pageX/pageY:当前鼠标触发点距离当前页面左上角x/y轴坐标,它与clientX/clientY的区别是它们不随滚动条的位置变化;
<div id="box"></div>
<script>
let div = document.getElementById("box");
div.addEventListener("click", (event) => {
console.log(`Page coordinates: ${event.pageX}, ${event.pageY}`);
});
</script>
复制代码
在页面没有滚动时,pageX和pageY与clientX和clientY的值相同。
- screenX/screenY:表示鼠标指针相对于屏幕的水平和垂直坐标;
<div id="box"></div>
<script>
let div = document.getElementById("box");
div.addEventListener("click", (event) => {
console.log(`Screen coordinates: ${event.screenX}, ${event.screenY}`);
});
</script>
复制代码
- offsetX/offsetY: 表示相对于定位父级的水平和垂直坐标当页面无定位元素时,body是元素的定位父级。由于body的默认margin是8px,所以offsetX/Y与clientX/Y差(8,8)。
示例:鼠标点击出现小圆圈效果
<a href="https://www.baidu.com/">click me</a>
<button>btn</button>
<script>
let link = document.querySelector('a');
link.onclick = function (event) {
event.preventDefault();
}
let btn = document.querySelector('button');
btn.onclick = function (event) {
console.log('button');
event.stopPropagation();//如不调用,则click事件会传播到document.body上
}
document.body.onclick = function (event) {
console.log('body clicked');
}
</script>
复制代码
//css样式
<style>
div { width: 10px; height: 10px;border-radius: 50%;border: 2px solid #cccccc;position: absolute;left: 0;top: 0;display: none;}
</style>
//html & js
<div></div>
<div></div>
<div></div>
<script>
let divs = document.querySelectorAll("div");
//创建一个move函数,obj代表你要运动的哪个div,ev代表事件对象
function move(obj, ev) {
obj.style.display = 'block';
var opc = 100;
var timer = setInterval(function () {
//宽高慢慢变大
obj.style.width = obj.offsetWidth + 5 + 'px';
obj.style.height = obj.offsetHeight + 5 + 'px';
//每走一次透明度要变化
obj.style.opacity = (opc -= 10) / 100;
obj.style.left = ev.clientX - obj.offsetWidth / 2 + 'px';
obj.style.top = ev.clientY - obj.offsetHeight / 2 + 'px';
//清除定时器
if (opc <= 0) {
clearInterval(timer);
//在定时器停的时候,把它们的值都回到最初的状态,为了下一次点击还从最初的状态开始运动
obj.style.width = '10px';
obj.style.height = '10px';
obj.style.opacity = 0;
}
}, 17);
}
//调用move函数
document.onclick = function (ev) {
//走第一个圈
move(divs[0], ev);
//走第二个圈
setTimeout(function () {
move(divs[1], ev);
}, 1000);
// 走第三个圈
setTimeout(function () {
move(divs[2], ev);
}, 2000);
}
</script>
复制代码
4.4 键盘事件
键盘事件是用户操作键盘时触发的,包含了以下三个事件:
事件名称 | 描述 |
---|---|
keydown | 键盘按下 |
keypress | 键盘长按(DOM3event废弃了keypress,推荐使用textInput) |
keyup | 键盘抬起 |
虽然所有元素都支持这些事件,但当用户在文本框中输入内容时最容易看到。当用户按下键盘上的某个字符键时,首先会触发 keydown事件,然后触发keypress事件,最后触发keyup事件。注意keydown,keypress事件会在文本框出现变化之前触发,而keyup事件会在文本框出现变化之后触发。如果一个字符键被按住不放,keydown和keypress就会重复触发,直到这个键被释放。对于非字符键,在键盘上按一下这个键,会先触发 keydown事件,然后触发keyup事件。如果按住某个非字符键不放,则会重复触发 keydown事件,直到这个键被释放,此时会触发 keyup事件。
对于keydown和keyup事件,event对象的keyCode属性中会保存一个键码,对应键盘上特定的一个键。对于字母和数字键, keyCode的值与小写字母和数字的 ASCII 编码一致。
<input type="text" id="myText">
<script>
let textbox = document.getElementById('myText');
textbox.addEventListener('keyup', (event) => {
console.log(event.keyCode);
})
</script>
复制代码
4.5 移动端触摸事件
事件名称 | 描述 |
---|---|
touchstart | 手指触摸到一个DOM元素时触发 |
touchmove | 手指在一个DOM元素上滑动时触发 |
touchhead | 手指从一个DOM元素上移开时触发 |
touchcancel | 操作取消 |
4.6 其它事件
事件名称 | 描述 |
---|---|
error | 资源加载失败时 |
change | 内容改变 |
readystatechange | ajax请求状态改变事件 |
五、内存与性能
5.1 事件委托
在js中页面事件处理程序的数量与页面整体性能直接相关,所以当有过多事件处理程序时,会损耗大量性能,这时我们可以用到“事件委托”机制,也就是利用事件会在冒泡阶段向上传播这一特点,把子节点的事件处理程序定义在父节点上,由父节点只使用一个事件处理程序来管理一种类型的事件。如click事件冒泡到document,这样可以为整个页面指定一个onclick事件处理程序而不用为每个可点击元素分别指定事件处理程序。
<ul id="list">
<li id="list1">1</li>
<li id="list2">2</li>
<li id="list3">3</li>
</ul>
<script>
let list = document.getElementById("list");
list.addEventListener("click", (event) => {
let target = event.target;
switch (target.id) {
case "list1":
console.log('hello');
break;
case "list2":
console.log('world');
break;
case "list3":
console.log("hello world");
break;
}
});
</script>
复制代码
像上面代码所示,只给ul元素添加一个onclick事件处理程序,而所有列表项都是它的后代,列表项身上的事件会向上冒泡,最终都会由这个函数来处理。因为事件目标是每个被点击的列表项,所以只检查event对象的id属性就可,然后再执行相应操作。
只要可行,就应该考虑只给document添加一个事件处理程序,通过它处理页面中所有某种类型的事件。好处在于document对象随时可用,任何时候都可以给它添加事件处理程序。节省了花在设置页面事件处理程序上的时间。减少整个页面所需的内存,提升整体性能。
最适合使用事件委托的事件包括:click、mousedown、mouseup、keydown和keypress。
5.2 删除事件处理程序
把事件处理程序指定给元素后,在浏览器代码和负责页面交互的JavaScript代码之间就建立了联系。这种联系建立得越多,页面性能就越差。除了通过事件委托来限制这种连接之外,还应该及时删除不用的事件处理程序。第一个是删除带有事件处理程序的元素。比如通过真正的DOM方法removeChild()或replaceChild()删除节点。最常见的还是使用innerHTML整体替换页面的某一部分。这时候,被innerHTML删除的元素上如果有事件处理程序,就不会被垃圾收集程序正常清理。
<div id="box">
<button id="btn">btn</button>
</div>
<script>
let btn = document.getElementById('btn');
btn.onclick = function () {
//do something
btn.onclick = null;//删除事件处理程序
document.getElementById('box').innerHTML = 'xxxxx';
}
</script>
复制代码
在上面的代码中设置div元素的innerHTML属性前,按钮事件处理程序先被删除,这样就可以确保内存被回收,按钮也可以安全的从DOM中删掉。