事件机制
- 关键词
- 事件捕获
- 事件冒泡
- 事件流
事件流
JS与HTML的交互是用事件实现的,事件流描述了页面接收事件的顺序。
事件冒泡
从具体的元素开始触发事件,向上传播。
所有的现代浏览器都支持
。
事件捕获
与事件冒泡相反,从上方的结点先接收事件,向下传播至具体的结点。
旧版的浏览器不支持
。
DOM事件流
DOM2 Events 规范规定事件流分为 3 个阶段: 事件捕获
、到达目标
和 事件冒泡
。
事件捕获最先发生,为提前拦截事件提供了可能。然后,实际的目标元素接收到事件。最后一个阶段是冒泡,最迟要在这个阶段响应事件。
所有现在浏览器都支持 DOM 事件流,只有 IE8 以及更早的浏览器不支持
。
整个事件流中监听事件只能被触发一次?❎
只要没有显示的阻止( stopPropagation
) 事件传递,那么就会按照事件流传递。
事件处理程序
为了响应执行的某种动作( click
、 load
、 mouseover
... )而调用的 on 开头的函数被称为事件处理程序(事件监听器)。
HTML事件处理程序
<input type="button" value="click me" onclick="console.log('click')" />
浏览器会先创建一个函数来封装属性的值,这个函数有一个特殊的局部变量:event
用来保存 event
对象,函数中 this
指向事件的目标元素。
DOM0事件处理程序
const btn = document.getElementById("myBtn");
btn.onclick = function(){
console.log('Clicked')
}
把一个函数赋值给 DOM 元素的事件处理程序的属性
。兼容性最好,所有的浏览器都支持此方法。
这种事件处理是冒泡阶段的。
所赋值的函数被视为元素的方法,在元素的作用域中运行,this
指向该元素本身。可以通过 this 访问元素的任何属性和方法。
将事件处理程序属性设置为 null,即可移除该事件处理程序。
btn.onclick = null;
如果有多个 DOM0 事件处理程序的话,后面的是会把前面的给覆盖掉。只有执行最后一个调用的结果。
DOM2事件处理程序
在DOM 节点上通过 addEventListener()
和 removeEventLinstener()
来添加和移除事件处理程序。
target.addEventListener(type, listener, options | useCapture);
type
事件名listener
事件处理函数options
capture
表示 listener 会在该类型的事件捕获阶段
传播到该 EventTarget 时触发once
listener调用一次后移除passive
设置为true时,表示listener
永远不会调用preventDefault()
。如果listener 仍然调用了这个函数,客户端将会忽略它并抛出一个控制台警告。
useCapture
指定函数执行是在事件流的捕获阶段
还是冒泡阶段
,默认false
则为冒泡阶段
,但对事件的传播是没有影响的。
DOM2 事件处理程序的一个优点是可以给一个元素添加多个
事件处理程序,并按添加的顺序触发
。
使用addEventListener()
添加的事件处理程序只能使用 removeEventLinstener()
移除(三个参数均一致才可以
);所以,使用匿名函数添加的事件处理程序是不能被移除的。
Event
接口的**preventDefault()**
方法,告诉user agent:如果此事件没有被显式处理,它默认的动作也不应该照常执行。此事件还是继续传播,除非碰到事件侦听器调用stopPropagation()
或stopImmediatePropagation()
,才停止传播。
IE事件处理程序
IE 实现事件处理程序的方法是: attachEvent()
和 detachEvent()
这两个方法接收两个同样的参数:事件处理程序的名称( eg: onclick
)和事件处理函数。
因为 IE8 及更早的版本只支持事件冒泡,所以使用 attachEvent()
添加的事件处理程序是添加在冒泡阶段。
注意:
- 作用域:
attachEvent()
是在全局作用域中运行的,所以attachEvent()
中的函数中的this
是window
;- 执行顺序:IE 事件处理程序的执行顺序是和添加顺序相反的。
事件对象
所有的浏览器都是支持这个 event
对象
DOM事件对象event
在事件处理函数内部,this
对象始终等于 currentTarget
,因为 this 是指向调用的对象的;而target
是事件触发的实际目标,可能存在两者不相等的情况。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="div"> Click me</div>
</body>
<script>
var div = document.querySelector('#div');
div.addEventListener('click', function(e){
console.log('div click', e.currentTarget === this); // true
console.log('div click', e.target === this); // true
})
var body = document.querySelector('body');
body.addEventListener('click', function(e){
console.log('body click', e.currentTarget === this); // true
console.log('body click', e.target === this); // false
})
</script>
</html>
几个API
preventDefault()
event.preventDefault()
方法用于阻止事件的默认行为(比如,a 标签有跳转到 href 链接的默认行为,使用 preventDefault()
可以阻止这种导航行为)
preventDefault()
阻止的必需是可 cancelable
的元素 **
Event
事件的preventDefault
正常规范操作是先check是否可以cancelable
(MDN check),如果cancelable=false
,那就不能被阻止默认行为,这个代码就不能起作用。(参考MDN:大部分由用户与页面交互产生的原生浏览器事件都可以被取消。取消click,scroll 或 beforeunload 事件将分别阻止用户点击某些元素,滚动页面或跳离页面。)
stopPropagation()
event.stopPropagation()
方法用于立即阻止事件流在 DOM 中的传播,取消后续的事件捕获或冒泡。
scroll事件
- 对于普通Element元素是不冒泡的
document
的defaultView
的scroll
事件冒泡,将body
内放一个超出一屏的 div,然后监听 window 和 此 div 的 scroll 事件就可以
IE事件对象
- 根据使用的事件处理程序不同而不同
- 使用 DOM0 事件处理程序,event 对象是全局对象 window 的一个属性
- 使用
attachEvent()
/ HTML 属性方法处理事件处理程序,event 对象会作为唯一的参数传给处理函数(event 仍然是 window 对象的属性,只是方便将其作为参数参入)
如何防止弹窗后底层元素滚动
当我们滚动鼠标滚轮,或者滑动手机屏幕时,触发对象可分为两种类型(详见W3C规范):
viewport
被触发滚动, eventTarget 为关联的Document
element
元素被触发滚动,通常也就是我们添加overflow 滚动属性
的 element 元素, eventTarget 为相应的node element
方案一:preventDefault 阻止弹出层默认的滚动事件
当我们触发滚轮或滑动时,如果当前元素没有设置 overflow 这样的属性
,同时也没有 preventDefault 掉原生的滚动/滑动事件,那么此时触发的是 viewport 的滚动
,position:fixed 的元素并没有什么例外。
那么如果不是事件冒泡
,为什么会发生滚动穿透的问题呢?如果当前滚动的元素可以滚动,那么就会在当前元素触发 scroll 事件。如果当前的元素不能滚动(或滚动到边界不能再继续滚动),那么就会滚动外层元素。
因此,我们可以滚动到边界(上 下 左 右)的时候,如果继续滚动就preventDefault
。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
body {
position: relative;
width: 100vw;
height: 10000px;
margin: 0;
background-image: linear-gradient(#e66465, #9198e5);
}
#mask {
position: fixed;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.6);
top: 0;
left: 0;
}
.modal {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 50vh;
background: #fff;
overflow: scroll;
}
</style>
</head>
<body>
<div id="mask">
<div class="modal">
<h1>A</h1>
<h1>B</h1>
<h1>C</h1>
<h1>D</h1>
<h1>E</h1>
<h1>F</h1>
<h1>G</h1>
<h1>H</h1>
<h1>I</h1>
<h1>G</h1>
<h1>K</h1>
<h1>L</h1>
<h1>M</h1>
</div>
</div>
</body>
<script>
var mask = document.querySelector('#mask');
var modal = document.querySelector('.modal');
// 记录初次touch纵坐标
let startY = 0;
const modalHeight = modal.clientHeight;
const modalScrollHeight = modal.scrollHeight;
modal.addEventListener("touchstart", (e) => {
startY = e.touches[0].pageY;
})
mask.addEventListener("touchmove", (e) => {
let endY = e.touches[0].pageY;
let delta = endY - startY;
// 滚动到边界(上 下)的时候,如果继续滚动就preventDefault()。
if (
(modal.scrollTop === 0 && delta > 0) ||
(modal.scrollTop + modalHeight === modalScrollHeight &&
delta < 0)
) {
e.preventDefault()
}
}, true);
</script>
</html>
方案二:设置底层元素的 overflow: hidden
在弹窗弹出的时候,手动修改 body 的样式,然后记得在弹窗关闭的时候将 body 的样式更改回去。
事件代理
如果一个节点中的子节点是动态生成的,那么子节点需要注册事件的话应该注册在父节点上。
事件代理的方式相较于直接给目标注册事件来说,有以下优点:
- 节省内存
- 不需要给子节点注销事件
<ul id="ul">
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>
<script>
let ul = document.querySelector('#ul');
ul.addEventListener('click', (event) => {
// 注意是target
console.log(event.target);
});
</script>
文章参考链接: 深入理解浏览器的事件机制