xinglie / xinglie.github.io

blog
https://xinglie.github.io
153 stars 22 forks source link

如何同步多个div的scrollTop或scrollLeft #89

Open xinglie opened 2 years ago

xinglie commented 2 years ago

假设有2divAB,它们的clientHeight相同,scrollHeight也相同,如何在滚动A的时候,B也同步滚动?即AB的滚动条位置相同?

这个问题看上去很简单,我们只要监听Ascroll事件,在这个事件里面设置BscrollLeftscrollTopA相同即可

A.addEventListener('scroll',() => {
    B.scrollTop = A.scrollTop;
},{
    passive:true
});

事实上这个方案虽然简单,但在遇到复杂的页面时:有动画、滚动内容很多等情况下,浏览器已经开始掉帧,那么scroll事件的触发就会被延迟,表现为A已经滚走了一部分内容,而B还在原来的位置上,AB的滚动并不同步。

这个问题在移动端表现更为明显,因为scroll事件是在滚动结束才触发,许多依赖scroll事件做的虚拟列表都会有该问题,比如这个:https://stackoverflow.com/questions/62489980/virtual-scroll-shows-white-spaces-on-mobile-devices-with-fast-kinetic-scrolling

那么我们该如何解决这个问题,让多个滚动容器同步的显示相应的区域呢?即使卡顿、掉帧的情况下也依然是同步的?

我从vscode中找到了方案

vscode的输入区域和滚动条完全是自己做的,这样何时滚动、滚动多少完全是自己控制的,在这种情况下,同步多个滚动容器且保持一致是完全可行的。

比如这里:https://github.com/microsoft/vscode/blob/main/src/vs/base/browser/mouseEvent.ts 进行了鼠标事件的封装,尤其是wheel事件的处理

在这里 https://github.com/microsoft/vscode/blob/main/src/vs/base/browser/ui/scrollbar/scrollableElement.ts#L360 进行了滚动处理。

只不过vscode对滚动区域和滚动条都是自己绘画控制的,难道我们也自己去做滚动条吗?

事实上我们只需要响应wheel事件,阻止默认行为即可,而移动端则需要响应touchstart等事件,同样的阻止默认行为,这样我们带有滚动条的容器就不会滚动了,除非拖动滚动条,而我的测试来看,如果是拖动滚动条滚动的话,是没出现不同步的情况的。

vscode处理touch事件可参考这里:https://github.com/microsoft/vscode/blob/main/src/vs/base/browser/touch.ts

所以我们只需要阻止默认的滚动行为,转而变成我们自己控制滚动即可。

A.addEventListener('scroll', () => {
    B.scrollTop = A.scrollTop;
}, {
    passive: true
});
A.addEventListener('wheel', (e: WheelEvent) => {
    e.preventDefault();
    let { deltaMode, deltaY, DOM_DELTA_PIXEL } = e;
    if (deltaMode != DOM_DELTA_PIXEL) {
        deltaY = deltaY > 0 ? 1 : -1
    }
    A.scrollTop += deltaY * 2;
}, {
    passive: false
})