本文详解如何使用原生JavaScript实现多个容器滚动条的同步跟随。首先分析直接同步scrollTop导致的相互触发问题,对比mousewheel与scroll事件的优劣。通过监听鼠标位置判断主动滚动源,解决事件冲突。最后推导内容高度差异时的按比例滚动公式,实现编辑与预览窗口的平滑同步。
同步滚动需求与DOM结构
在许多支持使用 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;
}
.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
})
表面上看一切正常,但实际情况并非如此简单。
此时若使用鼠标滚轮滚动,会发现滚动变得吃力,两个容器元素的滚动似乎受到阻碍,难以顺畅进行。
仔细分析可知,原因在于:当在左侧滚动时,触发左侧的滚动事件,导致右侧跟随滚动;而右侧的跟随滚动本身也是一个滚动行为,因此又触发了右侧的滚动事件,进而导致左侧再次跟随滚动……如此循环,形成相互触发的死锁,导致滚动卡顿。
解决事件冲突:mousewheel与scroll策略
解决 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>l.top) {
// 进入 .left元素中
}
这种方法可行,但考虑到两个滚动容器几乎占据整个屏幕, mousemove 事件监听的区域过大,对性能要求较高。因此,可改用 mouseover 事件,只需监听鼠标是否进入某个滚动容器即可,从而省去坐标判断步骤。
addEventListener('mouseover',function(){
// 进入 .left滚动容器元素内
})
当确定鼠标主动滚动的容器元素后,只需处理该容器的滚动事件,而对另一个跟随滚动容器的滚动事件不做处理。
效果良好,性能优异, perfect ,可以结束开发。
内容高度不一致时的按比例滚动
按比例滚动
上述示例均基于两个滚动容器内容高度完全一致的情况。若两个容器内容高度不同,结果如何?
效果如下所示:
可见,由于两个容器内容高度不同,最大 scrollTop 值也不同。当其中一个 scrollTop 值较小的元素滚到底部时,另一个元素可能仅滚动至一半;或者当其中一个 scrollTop 值较大的元素仅滚动一半时,另一个元素已滚到底部。
这种情况十分常见,例如在使用 markdown 写作时,一个一级标题标记 # 在编辑模式下占用的高度通常小于预览模式下的高度,从而导致左右两侧滚动高度不一致。
因此,若需考虑这种情况,便不能简单地为两个滚动容器相互设置 scrollTop 值。
虽然无法固定滚动容器内容的高度,但可以确定:滚动条的最大滚动高度,即 scrollTop 的值,与滚动容器内容高度及容器本身高度之间存在一定关系。
为了获取滚动容器内容的高度并保留滚动条,需给容器元素添加一个子元素,子元素高度不限,其高度即为滚动容器内容的高度。容器高度固定,溢出部分滚动即可。
<div id="container">
<div class="left">
<div class="child"></div>
</div>
<div class="right">
<div class="child"></div>
</div>
</div>
结构示例如下:
通过观察推论与实践验证,已确定它们之间的关系,即最基本的加减法运算:
滚动条的最大滚动高度(scrollTopMax) = 滚动容器内容的高度(即子元素高度ch) - 滚动容器本身的高度(即容器元素高度ph)
即
也就是说,若已确定滚动容器内容高度(即子元素高度 ch)与滚动容器本身高度(即容器元素高度 ph),则一定能确定滚动条的最大滚动高度( scrollTop )。由于这两个高度值均可获取,故可计算出 scrollTop。
因此,若要使两个滚动元素容器等比例上下滚动,即一个元素滚到头或滚到底时,另一个元素也能对应滚到头和滚到底,只需获取这两个滚动容器元素之间 scrollTop 最大值的比例( scale )即可。
确定 scale 后,在实时滚动时,只需获取主动滚动容器元素的 scrollTop1,即可计算出另一个跟随滚动容器元素对应的 scrollTop2:
思路清晰后,编写代码便轻而易举,效果如下:
非常顺滑。
总结与扩展
小结
上述内容基本实现了需求。在实际应用中,可能需根据具体情况稍作调整。例如,若编写一个 markdown 的在线编辑和预览页面,需根据输入内容的高度实时更新 scale 值。不过主体逻辑已完备,小修小改并无难度。
此外,本文所述不仅适用于两个滚动容器元素的跟随滚动,还可扩展至多个元素间的跟随滚动。本文仅为便于讲解而具体到两个元素的情况。
总结
以上所述是小编给大家介绍的原生JS控制多个滚动条同步跟随滚动效果,希望对大家有所帮助。如果大家有任何疑问请留言,小编会及时回复。在此也非常感谢大家对本站的支持!