最近在写组件,遇到的第一个问题就是浏览器的滚动条。
记得之前就被滚动条长痛过一次了,这次决定短痛, 直接总计来写一个滚动条。
什么?为什么要这么大费周章?滚动条这个磨人的小妖精,我觉得她值得...
最近写 Table 的虚拟化组件,因为 Table 实际是计算出位置的一个个 div 拼成的 Grid,所以在整个组件中,位置极为重要。
但是滚动条它出现了。最初你以为它是一个小天使,在你需要滚动的时候出现,没有滚动的时候消失,大概是这个样子的...

不不不不不不~ 他实际是这个样子的

甚至能在同一个浏览器窗口内出现4个滚动条....
甚至还区分系统,在一些老版本的windows系统中它甚至是这个样子的

WTF!

首先我们先搭个架子
// App.js
{% raw %}
import "./TestBody.css";
function App() {
const testData = new Array(50).fill('item');
return (
<div className="container">
<div className="testContainer">
<div className="testBody">
{testData.map((item, index) => (
<div key={index} className="testItem">{`${item} - ${index}`}</div>
))}
</div>
</div>
</div>
);
}
export default App;
{% endraw %}说实话干掉滚动条的方法还值得商榷,因为当前开发的项目中本身就有对滚动条的样式更改。所以我这里只是单纯的给出自己的禁用原始滚动条的方法,仅供参考。
/* TestBody.css */
::-webkit-scrollbar {
display: none;
}首先我们先来明确一下滚动条由哪几部分组成。

可以看到滚动条是由 3 部分组成的
为了方便我默认 50 个 item,每个 item 高 50px,总高 2500px,视口高度为 300px
// App.js
{% raw %}
function App() {
const testData = new Array(50).fill('item');
return (
<div className="container">
<div className="testContainer">
<Scrollbar scrollbarContainerHeight
// App.js
{% raw %}
import React from 'react';
import "./scrollbar.css"
const Scrollbar = ({
scrollbarContainerHeight, // 滚动条容器高度
scrollbarRealHeight, // 滚动条真实的高度
}) => {
return (
<div className="scrollbar-viewer" style={{ height: scrollbarContainerHeight }}>
.scrollbar-viewer {
position: absolute;
user-select: none;
display: block;
right: 0;
width: 6px;
transition: background-color 0.2s ease;
z-index: 1;
}
.scrollbar-viewer:hover {

这样一个大概的滚动条组件架子就搭起来了。但是现在显然是没有任何效果的,我们 hover 上去只能看到滚动条的轨道却看不到滚动条本身的样子。
刚才说过滚动条的 dragger 的高度其实是与滚动条容器的高度(scrollbarContainerHeight) & 滚动条真实的高度(scrollbarRealHeight)有关。
其实就是一个简单的比例公式

其实根据这张图就能清楚的看出来比例关系了。
我们需要的是 dragger 的高度,draggerHeight其实在视口高度(也就是滚动条容器高度)的占比就应是视口高度在总高度中的占比。
通过下面这样一个公式就能求出draggerHeight

给出代码
const draggerHeight = useMemo(() => {
/**
* scrollbarContainerHeight x
* ------------------------ = ----------------------
* scrollbarRealHeight scrollbarContainerHeight
*/
return Math.pow(scrollbarContainerHeight, 2) / scrollbarRealHeight;
}, [scrollbarContainerHeight, scrollbarRealHeight]);就达到了如下效果

在上一步我们让滚动条的 dragger 有了正确的高度,但是滚动内容时滚动条显然还不会动,我们接下来就让他动起来!
怎么动呢???其实就是让dragger绝对定位,而我们只需要不断的根据滚动来计算top的偏移量即可。
首先我们需要先添加滚动事件
{% raw %}
function App() {
const testData = new Array(50).fill('item');
const [topOffset, setTopOffset] = useState(0);
const handleScroll = (e) => {
然后我们将topOffset作为参数传入滚动条组件,用于后续计算。

而滚动条的偏移量我们看这张图,偏移距离在视口高度的占比就是滚动偏移量在总高度中的占比

const draggerTop = useMemo(() => {
/**
* x currentTopOffset
* ----------------------- = --------------------
* scrollbarContainerHeight scrollbarRealHeight
*/
return (topOffset * scrollbarContainerHeight) / scrollbarRealHeight;
}, [topOffset, scrollbarContainerHeight, scrollbarRealHeight]);然后写好之后兴奋的去滚动了一下内容,wait...滚动条根本没有出现啊。。。
我们漏了一个显示滚动条的逻辑(目前只有 hover 上去显示的逻辑)
滚动条什么时候需要显示呢?
所以我们需要知道当前是否在滚动,其实就是对比一下之前的topOffset和当前的topOffset就可以了。
然后需要注意的一点是,我们想要的效果是,当我们不滚动,滚动条会在一定时间后自动消失,这个我们只需要加一个定时器即可。
// Scrollbar.jsx
{% raw %}
const Scrollbar = ({
scrollbarContainerHeight,
scrollbarRealHeight,
topOffset,
}) => {
const [isShow, setIsShow] = useState(false);
const [isHover, setIsHover] = useState(false
可以看到现在实现的效果是这个样子,基本已经初具效果了。

但是还差亿点点就是滚动条我们是可以用鼠标拖拽的。
首先我们需要监听到鼠标按下的事件
{% raw %}
// 改造一下这里
const handleMouseOutScrollbar = () => {
if (!isMouseDown) {
setIsHover(false);
}
}
const handleMouseDown = () => {
setIsMouseDown(true);
}
{% endraw 需要注意的是之前我们写的监听鼠标移出指定区域的事件,现在,只要是按下鼠标的情况,就不能让滚动条消失,因为用户这个时候正在拖动滚动条。
但是遇到了新的问题,当鼠标移出规定区域,怎么获取鼠标按键抬起事件呢?否则滚动条将永久显示,这时候需要在全局的document上添加一个mouseup的事件监听,且我们只需要在isMouseDown = true的时候进行某些操作即可。
因为后面我们还要计算鼠标移动的距离之类的,所以这个时候再在document上加一个mousemove的事件监听。
const handleMouseUp = useCallback(() => {
if (isMouseDown) {
setIsMouseDown(false);
}
}, [isMouseDown]);
const handleMouseMove = useCallback(
(e) => {
if (isMouseDown) {
const $Element = document.getElementById("scrollbar-dragger-container");
然后就到了,整篇文章中我觉得最麻烦的地方了。还是先来一张图吧。

我们的任务,是根据鼠标拖拽的距离,来推算出dragger对应的偏移量,然后再加上dragger本身原有的偏移量即可。具体公式如下:

// App.js
const handleScrollTo = useCallback((offset) => {
const $Element = document.querySelector("#testContainer");
if ($Element) {
$Element.scrollTop = offset;
}
}, []);最终我们实现了如下效果。

说实话这次没啥可以参考的东西...跟着思路一步步来即可。