大家好,我是程序员路飞,三年的下水道打杂前端,喜欢研究技术,正在往全栈发展
欢迎小伙伴们加我微信:DZHningmeng
,一起讨论,期待与大家共同成长🥂
前言
有一天用antd4
的时候发现官网有一句话吸引了我
💯Select 使用了虚拟滚动技术,因而获得了比 3.0 更好的性能 , 生成更少的dom自然页面渲染速度更快了
💪于是我就想,虚拟滚动技术内部的实现原理是怎么样的,于是打算去网上搜索,但是发现没有文章讲解的能让我一眼就理解的,于是打算研究源码,自己用react
实现一个小的demo
antd4
的底层依赖了rc-virtual-list , 基于react-hook
实现
需要提前了解的知识
在线代码地址: virtual-list
大致的实现原理
下面我们通过两张图来大概理解下这个整体的图形结构和实现原理
当用户在rc-virtual-list
中用鼠标滚动滚轮,会触发onwheel
事件,我们处理的逻辑大致是以下两步
- 第一步我们改变完
holder
的scrollTop
之后,就是改变holder
里面内容的高度,本身是没有任何改变的,但是我们会发现item
的位置因为是相对定位,也会随着item-contianer
一起上移 - 我们把
item-container
的translateY
的位置改变负数,item
就会下移到可视范围
但是大家会有一个疑问,那我们怎么知道要渲染的数据范围是哪些呢
- 通过
rc-virtual-list
的height
可以知道要渲染的数据有多少个,如果rc-virtual-lis
t的height
为300px
,item的高度为30px
, 我们可以渲染出11个,多出了一个是为了滚动内容会出现头部只有一部分的情况 - 然后可以根据
holder
的scrollTop
的高度可以计算出要从数组的哪一个位置过滤数据,然后将过滤后的数据循环展示到页面上
但是细心的朋友肯定就发现我们的滚动少了一个右边的滚动条,没有这个肯定是不行的
于是我们自画一个滚动条,并且加上自定义的拖动事件
rc-virtual-list-scrollbar
添加onMouseDown
事件,阻止冒泡和默认事件rc-virtual-list-scrollbar-thumb
添加onMouseDown
事件 阻止冒泡和默认事件 将状态改为滚动(为了防止滚动期间list
中的文字会被选中出现蓝色的背景) 绑定全局的mouseup
和mousemove
事件
代码实现关键点
鼠标滚轴事件
const onWheel = (e) => {
const { deltaY } = e;
syncScrollTop((top) => {
const newTop = top + deltaY / 12;
return newTop;
});
};
// refRc用useRef绑定到rc-virtual-list-holder
useEffect(() => {
refRc.current.addEventListener("wheel", onWheel);
return () => {
refRc.current.removeEventListener("wheel", onWheel);
};
}, [onWheel]);
// ================================ Scroll ================================
function syncScrollTop(newTop) {
setScrollTop((origin) => {
let value;
if (typeof newTop === "function") {
value = newTop(origin);
} else {
value = newTop;
}
const alignedTop = keepInRange(value);
refRc.current.scrollTop = alignedTop;// 这一步是通过ref改变了dom的scrollTop
return alignedTop; // 记录scrollTop是计算出其他页面渲染需要的数据
});
}
function calculate() {
let startIndex;
let endIndex; // 计算数据起始位置,方便过滤
let scrollbarHeight; // 自定义滚动轴的高度
let scrollbarTop; // 自定义滚动轴距离顶端的距离
let num = Math.round(height / itemHeight);
startIndex = Math.floor(scrollTop / itemHeight);
endIndex = startIndex + num;
scrollbarHeight =
(num / data.length) * height < 20 ? 20 : (num / data.length) * height;
scrollbarTop = (scrollTop / (itemHeight * data.length)) * height;
if (scrollbarTop > height - scrollbarHeight) {
scrollbarTop = height - scrollbarHeight;
}
return {
startIndex,
endIndex,
scrollbarHeight,
scrollbarTop,
};
}
自定义滚轴事件
const removeEvents = () => {
window.removeEventListener("mouseup", onMouseUp);
window.removeEventListener("mousemove", onMouseMove);
};
const patchEvent = () => {
window.addEventListener("mouseup", onMouseUp);
window.addEventListener("mousemove", onMouseMove);
};
// 用户鼠标按键被松开时执行
// 更改滚动状态为false, 移除全局的mouseup,onMouseMove事件
const onMouseUp = () => {
setScrollMoving(false);
removeEvents();
};
// rc-virtual-list-scrollbar-thumb滚动轴鼠标按下触发事件
// 更改滚动状态为true绑定事件
const onMouseDown = (e) => {
patchEvent();
pageY.current = getPageY(e);
setScrollMoving(true);
e.stopPropagation();
e.preventDefault();
};
// 计算滚轴滚动改变的高度
const onMouseMove = (e) => {
const movePageY = getPageY(e);
const gap = movePageY - pageY.current;
const rate = gap / height;
pageY.current = movePageY;
const newTop = refRc.current.scrollTop + rate * itemHeight * data.length;
syncScrollTop(newTop);
};
需要改善的地方
通过上面的原理和代码基本实现一个简单的虚拟列表,但是还有一个不足的地方就是滚动现在不够顺滑,item
的移动都是一整个移动,如果有小伙伴有好的方法可以评论区一起讨论,有其他问题可以一起交流
容易踩坑的地方
为了记录滚轮上一次的Y轴的位置,最初是用useState
保存的,但是由于会存在闭包问题,所以后面改为useRef
,不懂用户可以去官网查看,大致的用法就是这个变量不会因为函数的重新执行重新生成,它指向的始终是第一次刚生成的
结束语
通过研究virtual-list的源码了解到了虚拟列表的具体实现原理,平时只是调用组件,没有仔细深入研究,通过自己实现一个简单的demo掌握具体实现的方式,输出文档,给小伙伴一些有些的知识点,希望大家喜欢我的文章,希望认识到更多志同道合的伙伴
,如果你也对技术感兴趣,可以加我好友
,互相探讨
,一起进步
Github: Cheering-baby
公众号: 程序员路飞
vx: DZHningmeng
最后,如果喜欢我的文章,可以给个赞👍或者关注➕都是对我最大的支持