事件的使用
绑定事件
- DOM0 写法:
dom.on事件类型 = listener
- DOM2 写法:
dom.addEventListener('事件类型', listener, 配置对象)
listener
可以被指定为一个回调函数 / 对象(其 handleEvent()
方法用作回调函数)。
例如,一个可同时处理 fullscreenchange
和 fullscreenerror
事件的函数如下:
function eventHandler(event) {
if (event.type === 'fullscreenchange') {
/* 处理 fullscreenchange 事件 */
} else {
/* 处理 fullscreenerror 事件 */
}
}
配置对象
的可用属性:
capture
可选:默认为false
-在冒泡阶段触发回调;true
-在捕获阶段触发回调。once
可选:默认为false
;true
-触发回调之后自动移除事件。passive
可选:默认为false
;true
-表示告诉浏览器,回调中不会调用 preventDefault 方法。如果回调中仍然调用了 preventDefault 方法,客户端会忽略它并抛出一个控制台警告。signal
可选:AbortSignal,该 AbortSignal 的 abort 方法被调用时,监听器会被移除。
const controller = new AbortController();
const ele = document.querySelector('#box');
ele.addEventListener(
'click',
() => {
console.log('I am clicked');
},
{ signal: controller.signal }
);
controller.abort(); // 移除监听器
推荐使用 addEventListener 来注册事件监听器,理由如下:
- addEventListener 允许为一个事件添加多个监听器,所有的 listener 会按绑定顺序触发回调。
- addEventListener 可以控制 listener 的触发阶段,即可以选择捕获/冒泡阶段触发。
- addEventListener 的事件目标可以是一个文档上的元素 Element、Document 和 Window,也可以是任何支持事件的对象(比如 XMLHttpRequest),而不仅仅是 HTML / SVG 元素。
解除绑定
- DOM0 写法:
dom.on事件类型 = null
- DOM2 写法:
dom.removeEventListener('事件类型', listener)
唯一需要 removeEventListener 检测的是 capture
/ useCapture
标志。这个标志必须与 addEventListener 的对应标志匹配,其他标志则不需要。
passive
将 passive 设为 true 可以启用性能优化,它可以让开发者告诉浏览器,事件监听器中不会调用 event.preventDefault()
方法来阻止事件的默认行为。这样,浏览器就可以在两个线程里同时执行监听器中的 JS 代码和浏览器的默认行为,避免了等待和阻塞,提高了页面滑动的流畅度。
根据规范,addEventListener 的 passive 默认值始终为 false。但这可能会大大降低浏览器处理页面滚动的性能,因为触摸事件和滚轮事件的事件监听器在浏览器尝试滚动页面时,有可能会阻塞浏览器的主线程。
为了避免这一问题,大部分浏览器(Safari 除外)将文档级节点 Window、Document 和 Document.body 上的 wheel、mousewheel、touchstart 和 touchmove 事件的 passive 默认值更改为 true。如此,事件监听器便不能取消事件,也不会在用户滚动页面时阻止页面呈现。
批量绑定事件
li {
list-style: none;
width: 100px;
height: 100px;
background: lightcoral;
}
<ul id="box">
<li></li>
<li></li>
<li></li>
</ul>
let li = document.getElementsByTagName('li');
for (let i = 0; i < li.length; i++) {
li[i].addEventListener('click', () => {
// 排他, 性能消耗较大
for (let j = 0; j < li.length; j++) {
li[j].style.background = 'lightcoral';
}
li[i].style.background = 'lightblue';
});
}
改进:针对性排他
let li = document.getElementsByTagName('li');
let cur = -1; // 默认序号
for (let i = 0; i < li.length; i++) {
li[i].addEventListener('click', () => {
if (cur == i) {
return; // 节流
} else if (cur !== -1) {
li[cur].style.background = 'lightcoral';
}
cur = i; // 更新序号
li[cur].style.background = 'lightblue';
});
}
事件类型
鼠标点击事件
click
- - - 点击左键 / 按回车键(click
=mousedown
+mouseup
)contextmenu
- - - 点击右键(不常用:浏览器有默认的右键行为)dblclick
- - - 左键双击(ondblclick
= 2 *onclick
)mousedown
- - - 按下鼠标任意键mouseup
- - - 抬起鼠标任意键
鼠标移动事件
mouseenter
- - - 光标进入(不冒泡)mouseleave
- - - 光标离开(不冒泡)mouseover
- - - 光标进入mouseout
- - - 光标离开mousemove
- - - 光标移动(持续触发)
鼠标滚轮事件
wheel
- - - 鼠标在元素上滚动时触发(IE + Chrome)
document.onwheel = (event) => {
console.log(event.wheelDelta);
};
event.wheelDelta
:滚动方向,120
→ 上、-120
→ 下;滚动得较快时,可能会出现 120 的整数倍
火狐使用 DOMMouseScroll
,且只能通过 DOM2 的事件绑定方式使用
Div.addEventListener(
'DOMMouseScroll',
(event) => {
Div.innerHTML = event.detail;
},
false
);
火狐使用 event.detail
作为滚动方向:-3
→ 上;3
→ 下
兼容函数封装:
if (document.onwheel === null) {
// 给 Chrome 添加事件
document.onwheel = mouseWheelHandler;
} else {
// 给火狐添加事件
document.addEventListener('DOMMouseScroll', mouseWheelHandler, false);
}
function mouseWheelHandler(event) {
let direction;
if (event.wheelDelta) {
// Chrome 用的是 event.wheelDelta
direction = event.wheelDelta / 120;
} else if (event.detail) {
// 火狐用的是 event.detail
direction = -event.detail / 3;
}
console.log(direction);
}
键盘事件
keydown
键盘按下(所有键 = 输入键 + 功能键):
input.onkeydown = () => {
console.log(111);
};
如果一直按着不动,会持续触发该事件
keyup
键盘抬起:
input.onkeyup = function () {
console.log(111);
};
keypress
键盘按下(输入键,功能键不触发):
input.onkeypress = function () {
console.log(111);
};
常见的 keyCode
:
shift
、ctrl
、alt
对应的event.keyCode
为16
、17
、18
enter
对应的event.keyCode
为13
- 左上右下 →
37
、38
、39
、40
注意:keyCode
已被弃用,推荐使用 key
来判断用户按下的键值。
demo - 实现留言功能:
input.onkeyup = function (e) {
if (this.value.trim()) {
// 如果写了非空内容
if (e.keyCode == 13 && e.ctrlKey) {
// 同时按下 ctrl enter 键
let oLi = document.createElement('li');
oLi.innerHTML = this.value.trim();
if (oUl.children[0]) {
oUl.insertBefore(oLi, oUl.children[0]);
} else {
oUl.appendChild(oLi);
}
}
}
};
可以通过查看 ctrlKey
、altKey
、shiftKey
的值是否为 true
,来判断是否有按下 ctrl
、alt
、shift
键
demo - 实现方向键移动:
let oBox = document.getElementById('box');
document.onkeydown = function (e) {
// 给整个页面绑定事件
switch (e.keyCode) {
case 37:
oBox.style.left = oBox.offsetLeft - 10 + 'px';
break;
case 38:
oBox.style.top = oBox.offsetTop - 10 + 'px';
break;
case 39:
oBox.style.left = oBox.offsetLeft + 10 + 'px';
break;
case 40:
oBox.style.top = oBox.offsetTop + 10 + 'px';
break;
}
};
焦点事件
focus
- - - 得到焦点时触发(不冒泡)blur
- - - 失去焦点时触发(不冒泡)focusin
:focus
的冒泡版focusout
:blur
的冒泡版
用户界面事件
load
:在 window 上,当页面加载完成后触发;在 img 上,当图片加载完成后触发;在 object 元素上,当对象加载完成后触发select
:在文本框上,当用户选择了字符时触发resize
:文档视图调整大小时触发;在 window 上,当窗口被缩放时触发scroll
:文档视图或者一个元素在滚动时触发(区别于滚轮事件wheel
)input
:当一个input
、select
、textarea
元素的value
被修改时,会触发该事件change
:当一个input
、select
、textarea
元素的value
被修改并失焦时,会触发该事件
事件处理模型
- 单击某个子元素时,该元素称为事件源。
- 单击事件不仅发生在事件源上,也发生在其所有的父级上。
事件流:捕获阶段 + 冒泡阶段;事件流对应着事件的触发顺序(先捕获、后冒泡)
- 捕获阶段:父级 → 子级
- 冒泡阶段:子级 → 父级(浏览器会冒泡到 window 为止)
练一练:
#box1 {
width: 100px;
height: 100px;
background: lightgoldenrodyellow;
}
#box2 {
width: 50px;
height: 50px;
background: lightgreen;
}
<div id="box1">
<div id="box2"></div>
</div>
box1.addEventListener(
'click',
function () {
console.log('父级捕获');
},
true
);
box2.onclick = function () {
console.log('子级冒泡');
};
box2.addEventListener(
'click',
function () {
console.log('子级捕获');
},
true
);
box1.onclick = function () {
console.log('父级冒泡');
};
事件对象
JS 引擎会传入一个实参给事件处理函数,这个实参就叫做 “事件对象”。通常用形参 event
/ e
接收:
<div id="box">1</div>
<script>
box.onclick = function (event) {
console.log(event);
};
</script>
输出:PointerEvent { isTrusted: true, pointerId: 1, width: 1, height: 1, pressure: 0, … }
事件对象 event
中,封装了这次事件的很多细节。比如点击事件发生时,鼠标的位置:
document.addEventListener('mousemove', (event) => {
console.log('当前鼠标坐标为' + event.clientX + ',' + event.clientY);
});
常用方法
- stopPropagation - 阻止事件流。( propagation 有 “传播” 的意思 )
调用该方法后,事件就不会再沿着事件流继续往后触发:
box1.addEventListener(
'click',
function (event) {
console.log('父级捕获');
event.stopPropagation(); // 从这里开始阻止事件流
},
true
);
box2.addEventListener(
'click',
function () {
console.log('子级捕获');
},
true
);
box2.onclick = function () {
console.log('子级冒泡');
};
box1.onclick = function () {
console.log('父级冒泡');
};
此时点击子级也不会触发子级的点击事件。
- preventDefault - 阻止默认事件。
在浏览器中,某些元素的某些事件,会有自己的默认行为。比如:
- a 元素的 click 事件,会跳转页面
- form 元素的 submit 事件,会提交表单,最终导致页面刷新
调用 preventDefault 即可阻止这些默认行为:
<a id="link" href="http://www.baidu.com">百度</a>
<script>
link.onclick = function (event) {
event.preventDefault(); // 超链接的默认点击事件是跳转
console.log(111);
};
</script>
此时点击超链接也不会跳转页面了。
常用属性
event.type
- 事件的类型。eg:click
event.target
- 事件源。即使该元素身上没有监听,也是返回它。event.currentTarget
- 当前元素,即this
的指向。event.button
- 区分鼠标左、中、右键(0、1、2)。
常用信息
-
offsetX
/offsetY
- - - 鼠标距离 [事件对象] 的左上角 -
layerX
/layerY
- - - 鼠标距离 [最近的定位元素] 的左上角(会一直找到 body 标签;自身有定位属性的话就是相对于自身) -
clientX
/clientY
- - - 鼠标距离 [显示区域] 的左上角(与页面是否有滚动无关) -
pageX
/pageY
- - - 鼠标距离 [页面] 的左上角(会加上滚动的距离) -
screenX
/screenY
- - - 鼠标距离 [计算机屏幕] 的左上角(在多屏显示的环境下,范围将增加到屏幕的组合宽高) -
x
/y
- - -clientX
/clientY
的别名
- 页面没有滚动时,pageXY 和 clientXY 的值相同
事件委托
通过事件对象 event 可以得到事件源 target。所以我们可以将子级的事件委托给父级处理。
<ul id="box">
<li>li1</li>
<li>li2</li>
<li>li3</li>
</ul>
<script>
box.onclick = (event) => {
console.log(event.target.innerHTML);
};
</script>
优点:① 不需要循环绑定每个子元素,可以优化性能;② 添加新元素时,不需要给新元素绑定事件。