事件穿透那点事
当我们有弹出层的时候都会遇到一个问题,在弹出层弹出之后,滚动页面,底层元素会跟着一起滚动。这肯定是我们不希望看到的。
浏览器的事件机制
我们面试的时候很常见的一个问题:浏览器的事件机制,关键词:事件捕获、事件冒泡、DOM Level 0事件、DOM Level 2事件
然后就 google 了下,虽然有很多人写有关时间机制的文章,但是感谢写的都不清不楚的。最后突然想起来,这种概念性的东西,还是要翻一翻字典呀!于是就翻了下红宝书(《JavaScript 高级程序设计》),果然对这些虽然是我们经常使用的东西有了更深入的了解。这大概就是温故而知新把,hhhhh
事件流
JS 与 HTML 的交互是用过事件实现的。事件流描述了页面接收事件的顺序。
事件冒泡
事件冒泡是 IE 团队提出的事件流方案,根据名字我们就可以看出,事件冒泡是从最具体的元素开始触发事件,然后向上传播至没有那么具体的元素(文档)。
事件捕获
事件捕获是 Netscpe 开发团队提出的事件流解决方案。和事件冒泡相反,事件捕获是从最不具体的节点最先接收事件,向下传播至最具体的节点。事件捕获实际上是为了在事件到达最终目标前拦截事件。
DOM 事件流
DOM2 Events 规范规定事件流分为 3 个阶段:事件捕获、到达目标和事件冒泡。事件捕获最先发生, 为提前拦截事件提供了可能。然后,实际的目标元素接收到事件。最后一个阶段是冒泡,最迟要在这个 阶段响应事件。
事件处理程序
为了响应用户或者浏览器执行的某种动作(click、load、mouseover... )而调用的 on开头的函数被称为事件处理程序(事件监听器)。
HTML 事件处理程序
特定的元素支持的每个事件都可以用 HTML 属性的形式使用事件处理程序。其实也就是我们最常使用的方式,如下代码,onclick 属性的值是 JS 代码或者其他的调用方法。
使用事件监听器,会先创建一个函数来封装属性的值,这个函数有一个特殊的局部变量:event 用来保存 event 对象,函数中的 this 指向事件的目标元素。
<input type="button" value="click me" onclick="console.log('click')" />
DOM0 事件处理程序
在 JavaScript 中创建事件监听器的传统方式是把一个函数赋值给 DOM 元素。兼容性最好,所有的浏览器都支持此方法。
每个元素(包括 window 和 document)都有时间处理程序的属性(一般都 onxxxx),这个属性的值为一个函数。
const btn = document.getElementById("myBtn");
btn.onclick = function(){
console.log('Clicked')
}
这样使用 DOM0 事件处理是发生在程序赋值时注册在事件流的冒泡阶段的。
所赋值的函数被视为元素的方法,在元素的作用域中运行,this 指向该元素本身。在事件处理程序中通过 this 可以访问元素的任何属性和方法。
将事件处理程序属性设置为 null,即可移除通过 DOM0 方式添加的时间处理程序。
btn.onclick = null;
多个事件处理程序的话,后面的是会把前面的给覆盖掉。只有执行最后一个调用的结果。
DOM2 事件处理程序
我们也可以通过在所有的 DOM 节点上通过 addEventListener()
和 removeEventLinstener()
来添加和移除事件处理程序。
addEventListener()
和 removeEventLinstener()
接收 3 个参数:事件名、事件处理函数 和 一个布尔值( true
表示在捕获阶段调用事件处理程序, false
(默认值)表示在冒泡阶段调用事件处理程序)。
const btn = document.getElementById("myBtn");
btn.addEventListener("click", () => {
console.log("clicked");
}, false);
btn.addEventListener("click", () => {
console.log("this.id")
});
以上代码为按钮添加了会在事件冒泡阶段触发的 onclick 事件处理程序。与 DOM0 类似,这个事件处理程序同样被附加在元素的作用域中运行。
使用 DOM2 事件处理程序的一个优点就是可以给一个元素添加多个事件处理程序,并按添加的顺序触发。
使用addEventListener()
添加的事件处理程序只能使用 removeEventLinstener()
移除(事件名、事件处理函数、最后一位布尔值都需要一致才可以);
所以,使用匿名函数添加的事件处理程序是不能被移除的。
**
大多数情况下,事件处理程序会被添加到事件流的冒泡阶段(也就是最后一个参数为 false
)因为跨浏览器兼容性好。
如果你需要在事件到达指定目标之前被拦截,那么可以将事件处理程序添注册到捕获阶段(最后一个参数设置为 true
)
IE 事件处理程序
IE 实现事件处理程序的方法是: attachEvent()
和 detachEvent()
这两个方法接收两个同样的参数:事件处理程序的名称( eg: onclick
)和事件处理函数。因为 IE8 及更早的版本只支持事件冒泡,所以使用 attachEvent()
添加的事件处理程序是添加在冒泡阶段。
const btn = document.getElementById("myBtn");
btn.attachEvent("onclick", function(){
console.log("Clicked");
})
IE 事件处理程序和 DOM2 事件处理程序有两个不一样的地方
- 作用域:
attachEvent()
是在全局作用域中运行的,所以attachEvent()
中的函数中的 this 是 window; - 执行顺序:IE 事件处理程序的执行顺序是和添加顺序相反的。
事件对象
在 DOM 中发生事件时,所有的相关信息都会被收集在一个名为 event 的对象中。这个对象包含了一些基本信息:触发事件的元素、事件的类型、以及一些与特定事件相关的其他数据(比如和鼠标事件相关的鼠标的位置信息)所有的浏览器都是支持这个 event 对象的。
btn.onclick = function(event){
console.log(event.type)
}
btn.addEventListener("click", () => {
console.log(event.type);
}, false);
DOM 事件对象
在事件处理函数的内部,this 对象始终等于 currentTarget
,但是 target
是事件触发的实际目标。(时间冒泡阶段可能出现 target
和 currentTarget
不相等的情况。
preventDefault()
方法用于阻止特点事件的默认行为(比如,a 标签有跳转到 href 链接的默认行为,可以阻止这种导航行为)
const link = document.getElementById("myLink");
link.onclick = function(event){
event.preventDefault();
}
stopPropagation()
方法用于立即阻止事件流在 DOM 结构中的传播,取消后续的事件捕获或冒泡。比如
const btn = document.getElementById("myBtn");
btn.addEventListener("click", () => {
console.log("clicked");
}, false);
document.body.addEventListener("click", () => {
console.log("body clicked");
}, false);
如果不调用 stopPropagation()
那么点击按钮会有两个 log 记录。如果加上的话,click 事件就不会传播到 body 上,只会有一个 log 记录。
IE 事件对象
IE 就是这么的与众不同(手动危笑)IE 事件对象是根据使用的事件处理程序不同而不同。
- 使用 DOM0 事件处理程序,event 对象是全局对象 window 的一个属性
- 使用
attachEvent()
/ HTML 属性方法处理事件处理程序,event 对象会作为唯一的参数传给处理函数(event 仍然是 window 对象的属性,只是方便将其作为参数参入)
防止弹窗底部滚动
那了解了浏览器的事件机制之后,回到开篇的问题:在弹出层弹出之后,滚动页面,底层元素会跟着一起滚动。
那么有以下几种解决方案
阻止事件穿透
其实方法刚刚也说到了,我们可以设置 addEventListener
的第三个参数 passive: true
:在捕获阶段调用事件处理程序 而不是在冒泡阶段调用事件处理程序。
设置 body 的 overflow: hidden
这种方式其实比较暴力,在弹窗弹出的时候,手动修改 body 的样式,然后记得在弹窗关闭的时候将 body 的样式更改回去。
// 底部不滑动
const bodyEl = document.querySelector('body');
let top = 0;
const stopBodyScroll = (isFixed: boolean) => {
if (isFixed) {
top = window.scrollY
bodyEl && (bodyEl.style.position = 'fixed')
bodyEl && (bodyEl.style.top = -top + 'px')
} else {
bodyEl && (bodyEl.style.position = '')
bodyEl && (bodyEl.style.top = '')
window.scrollTo(0, top) // 回到原先的top
}
}
这个方法还可以改进,因为我们每次不一定是触发的 body 的滚动,也可以是其他元素导致的滚动。其次是要记录被滚动元素之前的样式(position)不能一味的将其设置为空。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!