scroll事件会失效原因
scroll事件会失效原因
在Web开发过程中,或多或少都会使用到Scroll事件,用来做上拉加载、下拉刷新或者是延迟加载等等。
我在使用Scroll事件过程中经常遇到的问题必然是##Scroll为什么会失效##,解决方案无非是万能捕获阶段监听、overflow、高度问题,没有去深入了解产生原因是什么。
所以,趁着闲暇时间知其然知其所以然。
一点点从头捋一遍
JavaScript事件模型
JavaScript的事件模型有三种:
- DOM0事件模型(原始)
- DOM2事件模型
- IE事件模型
目前我们所熟知的事件模型是DOM2事件模型,也是我们重点讲的地方,它新增了冒泡和捕获概念,事件流也就有了三个阶段:捕获阶段-目标阶段-冒泡阶段
捕获阶段是事件从document到传递到目标元素的过程,而冒泡阶段是事件从目标元素传递到document的过程。
在平时,我们一般是监听事件的冒泡阶段,即elem.addEventListener('scroll', handler)。Vue中也提供了v-on指令监听某个事件,默认用的也是事件的冒泡阶段。
但是根据MDN对scroll事件的描述,发现了以前忽略的点:
冒泡: element的scroll事件不冒泡, 但是document的defaultView的scroll事件冒泡
这句话的意思就是说,如果scroll的目标元素是一个元素的话,比如说是一个div元素。那么此时事件只有从document到div的捕获阶段以及div的冒泡阶段。如果尝试在父级监视scroll的冒泡阶段监视这一事件是无效的。
如果scroll是由document.defaultView(目前document关联的window对象)产生的有冒泡阶段。但是由于其本身就是DOM树里最顶级的对象,因此只能在window里监视scroll的捕获阶段以及冒泡阶段。
当scroll的目标元素为document.defaultView时
在最常见的页面结构中来测试:
<body>
<div id="root">
<p>Hello world</p>
<p>Hello world</p>
....
</div>
<script>
window.addEventListener('scroll', e => console.log('window, capture'), true)
window.addEventListener('scroll', e => console.log('window,bubble'))
const root = document.getElementById('root')
root.addEventListener('scroll', e => console.log('root, capture'), true)
root.addEventListener('scroll', e => console.log('root,bubble'))
</script>
</body>
在控制台打印
"window, capture"
"window,bubble"
"window, capture"
"window,bubble"
......
可以看到在全程只有window上监听scroll的捕获阶段以及冒泡阶段的回调函数执行了。这验证之前的结论:
如果scroll是由document.defaultView(目前document关联的window对象)产生的有冒泡阶段。但是由于其本身就是DOM树里最顶级的对象,因此只能在window里监视scroll的捕获阶段以及冒泡阶段。
当scroll的目标元素是元素时
一个双栏布局,左侧为导航栏,右侧为内容,父容器使用Flex布局,要求导航栏不随内容的滑动而滑动。HTML文档如下:
<!DOCTYPE html>
<html>
<head>
<title>Scroll In Element</title>
<style>
body {
margin: 0;
}
#wrapper {
width: 100%;
height: 100vh;
display: flex;
overflow: hidden;
}
#left-col {
overflow: hidden;
flex: 0 0 300px;
background-color: aqua;
}
#right-col {
flex: 1;
overflow: auto;
}
</style>
</head>
<body>
<div id="wrapper">
<div id="left-col">
<nav>
barabara
</nav>
</div>
<div id="right-col">
<p>Hello world</p>
<p>Hello world</p>
......
</div>
</div>
<script>
const log = (elem, phase) => {
console.log(`${elem}, ${phase}`)
}
const bindEvent = (elem, elemName) => {
elem.addEventListener('scroll', log.bind(null, elemName, 'capture'), true)
elem.addEventListener('scroll', log.bind(null, elemName, 'bubble'))
}
bindEvent(window, 'window')
const wrapper = document.querySelector('#wrapper')
bindEvent(wrapper, 'wrapper')
const rightCol = document.querySelector('#right-col')
bindEvent(rightCol, 'rightCol')
</script>
</body>
</html>
此时scroll事件的目标元素是div#right-col。接下来我们,分别在window,div#wrapper,div#right-col上监听scroll的捕获阶段以及冒泡阶段。
在控制台打印:
"window, capture"
"wrapper, capture"
"rightCol, capture"
"rightCol, bubble"
"window, capture"
"wrapper, capture"
"rightCol, capture"
"rightCol, bubble"
如果scroll的目标元素是一个元素的话,比如说是一个div元素。那么此时事件只有从document到div的捕获阶段以及div的冒泡阶段
总结
综上所述,在使用scroll的时候,要思考以下两个点:
- scroll事件的目标元素是什么?也可以说谁产生了scroll事件。
- 监听scroll事件的元素是否在scroll事件的传递路径上,是否监听了正确的阶段?
其他相关问题
在Vue中子组件scroll失效问题
直接在子组件mounted里addEventListener进行监听
window.addEventListener('scroll', this.scrollEvent)
运用上述总结,先分析当前目标元素与目标元素是否在scroll事件传递路上。
我们很容易就分析出失效的原因,当前监听的是顶层window冒泡,目标元素是子组件,所以子组件的滚动根本不会被监听到。
解决方案
- this.$refs.childPage.addEventListener('scroll', this.scrollEvent)
- @scroll="scrollEvent"
- window.addEventListener('scroll', this.scrollEvent, true),万金油写法,在捕获阶段执行
未设置overflow:scroll / auto
// emmmm
未设置高度
在不确定父级高度一定情况下,使用*height:100%*也会导致scroll失效,稳妥的方案是100vh赋值,或者挨个检查父元素
解除scroll绑定失效问题
之前我就遇到了这个问题,甚至百思不得其解。
至于原因也很简单,捕获阶段添加了监听,我在冒泡阶段移除绑定。
window.addEventListener('scroll', this.scrollEvent, true)
// 错误示范
// window.removeEventListener('scroll', this.scrollEvent)
window.removeEventListener('scroll', this.scrollEvent, true)
v-on:scroll.passive
<!-- 滚动事件的默认行为 (即滚动行为) 将会立即触发 -->
<!-- 而不会等待 `onScroll` 完成 -->
<!-- 这其中包含 `event.preventDefault()` 的情况 -->
<div v-on:scroll.passive="onScroll">...</div>
我的理解滚动事件默认触发一次,不用在手动调用一次scroll方法
getBoundingClientRect
MDN解释
Element.getBoundingClientRect() 方法返回元素的大小及其相对于视口的位置。
我用该API来获取元素位置信息
const isIn = (el) => {
let bound = el.getBoundingClientRect()
const clientHeight = window.innerHeight
return bound.top <= clientHeight
}
使用 throttle 控制触发频率
引入 lodash:
import _ from 'lodash'
然后限制触发间隔为 500 ms:
window.addEventListener('scroll', _.throttle(() => {
let windowHeight = document.documentElement.clientHeight||window.innerHeight
let scrollTop = document.documentElement.scrollTop || document.body.scrollTop
let windowBottom = +scrollTop + +windowHeight
let selfTop = _.get(this.$refs, 'selfEcharts.offsetTop', 0)
if(windowBottom >= selfTop){
this.checkAndSetOption()
}
}, 500))
解绑事件
若想用 document.removeEventListener() 解绑事件,首先我们要抽离事件本身,将匿名函数转为实名函数。 为了保证 addEventListener 和 removeEventListener 时操作的是同一个函数,这里我们使用 data 添加实名函数:
data() {
return {
scrollEvent: _.throttle(this.checkPosition, 500)
}
},
mounted () {
window.addEventListener('scroll', this.scrollEvent)
},
methods: {
checkAndSetOption() {
let option = this.option
if (isValidOption(option)) {
this.myEcharts.setOption(option)
this.isOptionAbnormal = false
} else {
this.isOptionAbnormal = true
}
}
},
beforeDestroy() {
window.removeEventListener('scroll', this.scrollEvent, true)
}