原⽣JS控制多个滚动条同步跟随滚动效果
在⼀些⽀持⽤markdown写⽂章的⽹站,后台写作页⾯,⼀般都是⽀持markdown即时预览的,也就是将整个页⾯分成两部分,左半部分是你输⼊的markdown⽂字,右半部分则即时输出对应的预览页⾯,例如下⾯就是 CSDN 后台写作页⾯的markdown即时预览效果:
本⽂不是阐述如何从0实现这种效果的(后续很可能会单出⽂章,),抛开其他,单看页⾯主体中左右两个容器元素,即markdown输⼊框元素和预览显⽰框元素
本⽂要探讨的是,当这两个容器元素的内容都超出了容器⾼度,即都出现了滚动框的时候,如何在其中⼀个容器元素滚动时,让另外⼀个元素也随之滚动。
DOM 结构
既然是与滚动条有关,那么⾸先想到js中控制滚动条⾼度的⼀个属性:scrollTop,只要能控制这个属性的值,⾃然也就能控制滚动条的滚动了。
对于以下DOM结构:
<div id="container">
<div class="left"></div>
<div class="right"></div>
</div>
其中,.left元素是左半部分输⼊框容器元素,.right元素是右半部分显⽰框容器元素,.container是它们共同的⽗元素。
由于需要溢出滚动,所以还需要设置⼀下对应的样式(只是关键样式,⾮全部):
#container {
display: flex;
border: 1px solid #bbb;
js获取子元素}
.left, .right {
flex: 1;
height: 100%;
word-wrap: break-word;
overflow-y: scroll;
}
再向.left和.right元素中塞⼊⾜够的内容,让⼆者出现滚动条,就是下⾯这种效果:
样式是出来个⼤概了,下⾯就可以在这些DOM上进⾏⼀系列的操作了。
初次尝试
⼤致思路,监听两个容器元素的滚动事件,在其中⼀个元素滚动的时候,获取这个元素的scrollTop属性的值,同时将此值设置为另外⼀个滚动元素的scrollTop值即可。
例如:
var l=document.querySelector('.left')
var r=document.querySelector('.right')
l.addEventListener('scroll',function(){
r.scrollTop = l.scrollTop
})
效果如下:
似乎很不错,但是现在是不仅想让右边跟随左边滚动,还想左边跟随右边滚动,于是再加以下代码:
addEventListener('scroll',function(){
r.scrollTop = l.scrollTop
})
看上去很不错,然⽽,哪有那么简单的事情。
这个时候你再⽤⿏标滚轮进⾏滚动的时候,却发现滚动得有点吃⼒,两个容器元素的滚动似乎被什么阻碍住了,很难滚动。
仔细分析,原因很简单,当你在左边滚动的时候,触发了左边的滚动事件,于是右边跟随滚动,但是与此同时右边的跟随滚动也是滚动,于是也触发了右边的滚动,于是左边也要跟随右边滚动...然后就进⼊了⼀个类似于相互触发的情况,所以就会发现
滚动得很吃⼒。
解决 scroll 事件同时触发的问题
想要解决上述问题,暂时有以下两种⽅案。
将 scroll 事件换成 mousewheel 事件
由于scroll事件不仅会被⿏标主动滚动触发,同时改变容器元素的scrollTop也会触发,元素的主动滚动其实就是⿏标滚轮触发的,所以可以将scroll事件换成⼀个对⿏标滚动敏感⽽不是元素滚动敏感的事件:'mousewheel',于是上述监听代码变成了:
addEventListener('mousewheel',function(){
r.scrollTop = l.scrollTop
})
r.addEventListener('mousewheel',function(){
l.scrollTop = r.scrollTop
})
效果如下:
似乎是有点⽤,但是实际上还有两个问题。
当滚动其中⼀个容器元素的时候,另外⼀个容器元素虽然也跟着滚动,但滚动得并不流畅,⾼度有明显的瞬间弹跳
在⽹上了⼀圈,没有到关于wheel事件滚动频率相关内容,我推测这可能就是此事件的⼀个feature
⿏标每次滚动基本上都并不是以1px为单位的,其最⼩单元远⽐scroll事件⼩的多,我⽤我的⿏标在chrome浏览器上滚动,每次滚过的距离都恰好是100px,不同的⿏标或者浏览器这个数值应该都是不⼀样的,⽽wheel事件其实真正监听的是⿏标滚轮滚过⼀个齿轮卡点的事件,这也就能解释为何会出现弹跳的现象了。
⼀般来说,⿏标滚轮每滚过⼀个齿轮卡点,就能监听到⼀个wheel事件,从开始到结束,被⿏标主动滚动的元素已经滚动了100px,所以另外⼀个跟随滚动的容器元素也就瞬间跳动了100px
⽽之所以上述scroll事件不会让跟随滚动元素出现瞬间弹跳,则是因为跟随滚动元素每次scrollTop发⽣变化时,其值不会有
100px那么⼤的跨度,可能也没有⼩到1px,但由于其触发频率⾼,滚动跨度⼩,最起码在视觉上就是平滑滚动的了。
wheel只是监听⿏标滚轮事件,但如果是⽤⿏标拖动滚动条,就不会触发此事件,另外的容器元素也就不会跟随滚动了
这个其实很好解决,⽤⿏标拖动滚动条肯定是能触发scroll事件的,⽽在这种情况下,你肯定能够很轻易地判断出这个被拖动的滚动条是属于哪个容器元素的,只需要处理这个容器的滚动事件,另外⼀个跟随滚动容器的滚动事件不做处理即可。
wheel事件的兼容问题
wheel事件是DOM Level3的标准事件,但是除了此事件之外,还有很多⾮标准事件,不同的浏览器内核使⽤不同的标准,所以可能还需要按情况来进⾏兼容,具体可见MDN MouseWheelEvent
实时判断
如果你难以忍受wheel的弹跳,以及各种兼容,那么其实还有另外的路可以⾛得通,依旧是scroll事件,只不过需要做⼀些额外的⼯作。
scroll事件的问题在于,没有判断当前主动滚动的是哪⼀个容器元素,只要确定了主动滚动的容器元素,这事就好办了,例如上述使⽤wheel事件中,⽤⿏标拖动滚动条之所以能够使⽤scroll事件,就是因为能够很容易地确定当前主动滚动容器元素是哪⼀个。
所以,问题的关键在于,如何判断出当前主动滚动的容器元素,只要解决了这个问题,剩下的就很好办了。
不论是⿏标滚轮滚动还是⿏标按在滚动条上拖动滚动条滚动,都会触发scroll事件,并且这个时候,在坐标系Z轴上,⿏标的坐标肯定是位于滚动容器元素所占的⾯积之内的,也就是说,在Z轴上,⿏标肯定是悬浮或者位于滚动容器元素之上。
⿏标在屏幕上移动的时候,是可以获取到⿏标当前坐标的。
其中,clientX和clientY就是当前⿏标相对于视⼝的坐标,可以认为,只要这个坐标在某个滚动容器的范围内,则认为这个容器元素就是主动滚动容器元素,容器元素的坐标范围可以使⽤getBoundingClientRect进⾏获取。
下⾯是⿏标移动到.left元素中的⽰例代码:
if (e.clientX>l.left && e.clientX<l.right && e.clientY&p) {
// 进⼊ .left元素中
}
这样确实是可以的,不过考虑到两个滚动容器元素⼏乎占据了整个屏幕⾯积,所以mousemove所要监听的⾯积未免有点⼤,对于性能可能要求较⾼,所以其实可以换成mouseover事件,只需要监听⿏标有没有进⼊到某个滚动容器元素即可,也省去上述的坐标判断了。
addEventListener('mouseover',function(){
// 进⼊ .left滚动容器元素内
})
当确定了⿏标主动滚动的容器元素是哪⼀个时,只需要处理这个容器的滚动事件,另外⼀个跟随滚动容器的滚动事件不做处理即可。
嗯,效果很不错,性能也很好,perfect,可以收⼯喽~
按⽐例滚动
上述⽰例全部是在两个滚动容器元素的内容⾼度完全⼀致的情况下的效果,如果这两个滚动容器元素的
内容⾼度不同呢?
那就是下⾯这种效果:
可见,由于两个滚动容器元素的内容⾼度不同,所以最⼤的scrollTop也就不同,就会出现当其中⼀个scrollTop值较⼩的元素滚到底时,另外⼀个元素还停留在⼀半,或者当其中⼀个scrollTop值较⼤的元素才滚到⼀半时,另外⼀个元素就已经滚到底了。
这种情况很常见,例如你⽤markdown写作时,⼀个⼀级标题标记#在编辑模式下占⽤的⾼度,⼀般都是⼩于预览模式占⽤的⾼度的,这样就出现了左右两侧滚动⾼度不⼀致的情况。