当我们发现scroll事件不触发之后我们应该做什么

5,859 阅读6分钟

scroll事件不触发

为什么绑定的scroll事件失效了?为什么弹窗不随页面一起滚动?Vue无法监听scroll事件等等。

这类问题绝对是大家困惑最多的问题,我翻看了网上的一些解答,方案是可行的但是似乎大家都不清楚背后的原理和原因,甚至有一些解答几乎是错误的,但是这些错误观点竟然一传十十传百了。

本篇文章主要是说清楚scroll失效背后的真正的原因和本质,修正网上错误的观点。

问题一

问题:element.addEventListner('scroll', ()=> {})或者vue中对某个元素加@scroll滚动时不生效。

基础知识

image.png

大家都知道JavaScript的事件触发要经历捕获冒泡两个环节。以为scrollclick都一样没什么区别,也是要经历上图这个过程。

在MDN上查阅了scroll事件文档,发现文档上写scroll事件是也是会冒泡的。

image.png

但是实际上,从demo来看,是不会层层冒泡的,只会冒泡一次。

真正的原因

那为什么元素监听触发不了,windowdocument能触发呢?真正的原因就是我们搞错了scroll事件的用法和定义

我们来看一个MDN文档给出的关于元素绑定scroll事件的例子,我稍微改编了一下放在codesandbox里了

// css代码
div {
width: 4rem;
height: 8rem;
font-size: 3rem;
border: 1px solid #000000;
background-color: #ccc;
overflow: auto;
}
// js代码
const div = document.querySelector("div");
const log = document.getElementById("log");

div.addEventListener("scroll", logScroll);
function logScroll(e) {
    log.textContent = `Scroll position: ${e.target.scrollTop}`;
}
window.addEventListener("scroll", ()=>{console.log('window scroll')});

实现的功能是:当鼠标放在div元素上并滚动时,logScroll会执行并打印出scrollTop。如果元素绑定scroll事件不会冒泡的话,这个logScroll是不会执行的,但是Demo是执行了。

仔细观察CSS代码会发现我添加了overflow:auto,这个表示的意思是如果内容超出就变成overflow:scroll,也就是打开当前元素的滚动特性;如果内容不超出就变成overflow:visible,正常显示不出滚动条。

大家看到这里应该明白了scroll事件的用法和含义了吧。👇🏻

1.元素内部的内容区滚动才会触发scroll事件!!!(overflow属性很关键)

2.内容区的滚动动作会冒泡一次到它的父元素,之后就暂停冒泡了。(上面div可以触发scroll,window不行)

更重要的是理解scroll事件生效的条件是元素内部的内容区发生滚动。这个元素在页面里滚动了,它算哪个元素的内容区,那么那个元素就会触发滚动事件。

当我们在鼠标放在不属于div的区域,滚动鼠标时,是触发不了logScroll事件的,因为你这个时候滚动的是document元素或者说是window窗口里的内部的内容区,并不是div元素内部的内容区(也印证了div里的滚动事件冒泡到div就不会继续向上冒泡了)。

搞清楚scroll事件的定义之后,在使用时搞清楚滚动的内容区是什么,scroll事件就绑定在对应内容区的父元素上就行了。大部分情况都是绑定在最顶层的元素上的(window和document)。至于为什么继续看下面👇。

问题2

明明写了document.body.addEventListner('scroll', ()=> {})或者window.addEventListner('scroll', ()=> {}),鼠标滚动的时候都滚出火星子了就是不触发scroll事件,仿佛scroll失效了一样。

此时,你脑海的第一反应是这个事件会不会被覆盖了,当然不是。

混淆的概念之onscroll和onwheel

经过了上面一轮解释,你是不是有一种错觉:把scroll绑定在顶层元素上,那页面里只要滚动鼠标就能触发滚动事件了。

👆🏻上面的想法里混淆了onscrollonwheel的概念。

onwheel鼠标滚轮旋转,而 onscroll 处理的是对象内部内容区的滚动事件。

也就是说你鼠标滚轮滚动触发的是onwheel事件,scroll触发是因为你绑定对象的内部在滚动,和你鼠标滚没滚无关,这也是为什么在手机上你没有鼠标滑动屏幕也能触发scroll事件。而你鼠标滚动了,只能说明你触发了onwheel事件,并不一定会使得内部内容区发生滚动

排查问题方向

理解了上面的含义之后,当scroll失效的时候排查问题的方向应该是你绑定事件的对象内部的内容区到底有没有在滚动

1.document.body不触发scroll

image.png

有一种说法是:因为整个页面的滚动条是来自于htmloverflow:auto。由于body处在html的下一级的,那么绑定在body上并不生效。

通过上面这种说法,滚动的内容区是属于html的,那么绑定scroll到html上,理想情况是可以监听的。但是实际上不是,也是不会触发。下面的例子可以说明。

2.document.documentElement不触发scroll

html元素需要通过document.documentElement取到。绑定之后会发现依然不会触发scroll事件,说明整个页面的滚动条是来自于htmloverflow:auto这种说法并不准确。

那我们只能把这个假设再往上一级提,此时就到了documentdocument.addEventListner('scroll', (e)=> {console.log(e.target)})是可以触发scroll事件的。window也是会触发的。 我们打印一下触发scroll事件的target会发现e.target === document

这就说明每一个窗口页面天生的滚动属性是由document带来的。

3.最顶层window或document不触发scroll

我这里有个相关的失效demo

排查到这种程度是最令人费解的,都是顶层了居然还触发不了。我们还是不要忘记排查的主要目标是关注:内容区相对于绑定的元素到底有没有真正的滚动。

根据我的demo你要关注的点:

  • html和body的overflow-x: hidden;
  • html和body的height都为100%。

第一点:一般来说,我们都是Y方向的滚动,对于x方向的滚动overflow-x:hidden会觉得无所谓,觉得不会影响Y方向导致无法滚动,其实是有影响的,去掉之后会发现可以触发scroll了。

第二点:去掉html和body任何一个height为100%之后也可以触发滚动。不去掉时你会发现滚动触底的时候会触发两三次scroll。这是因为height:100%加上overflow:hidden把html元素内的内容区的滚动范围限制在页面窗口的大小,导致窗口变成了一个局部的滚动区(参考我第一个demo的div),所以局部滚动也不会冒泡上去。

4.为什么body使用addEventListner方式不触发scroll但是body.onscroll的方式可以触发

这个问题其实一直让我也很困惑。 所以我去问了chatGPT:

image.png

原来是浏览器的自动转换。

总结

关键点

  • 内容区是否是真的滚动 -> overflow属性
  • scroll事件的父元素绑对了吗 -> 内容区滚动只会冒泡一次到内容区的父元素,所以scroll要绑定在滚动区域的父元素
  • 是否把页面的滚动区变成了局部的滚动区

万能方法

因为scroll事件冒泡的特殊性,在捕获阶段去监听scroll:window.addEventListner('scroll', ()=> {}, true)。这样子可以保证页面内所有的scroll事件都能被监听到。