都2022年了,虚拟列表还不懂,快看过来👈

1,642 阅读5分钟

大家好,我是程序员路飞,三年的下水道打杂前端,喜欢研究技术,正在往全栈发展
欢迎小伙伴们加我微信:DZHningmeng,一起讨论,期待与大家共同成长🥂

前言

有一天用antd4的时候发现官网有一句话吸引了我

💯Select 使用了虚拟滚动技术,因而获得了比 3.0 更好的性能 , 生成更少的dom自然页面渲染速度更快了

💪于是我就想,虚拟滚动技术内部的实现原理是怎么样的,于是打算去网上搜索,但是发现没有文章讲解的能让我一眼就理解的,于是打算研究源码,自己用react实现一个小的demo

antd4的底层依赖了rc-virtual-list , 基于react-hook实现

需要提前了解的知识

  1. scrollTop 元素的内容垂直滚动的像素数
  2. onwheel 滚轮的滚动事件

在线代码地址: virtual-list

大致的实现原理

下面我们通过两张图来大概理解下这个整体的图形结构和实现原理

image.png

image.png

当用户在rc-virtual-list中用鼠标滚动滚轮,会触发onwheel事件,我们处理的逻辑大致是以下两步

  1. 第一步我们改变完holderscrollTop之后,就是改变holder里面内容的高度,本身是没有任何改变的,但是我们会发现item的位置因为是相对定位,也会随着item-contianer一起上移
  2. 我们把item-containertranslateY的位置改变负数,item就会下移到可视范围

但是大家会有一个疑问,那我们怎么知道要渲染的数据范围是哪些呢

  1. 通过rc-virtual-listheight可以知道要渲染的数据有多少个,如果rc-virtual-list的height300px,item的高度为30px, 我们可以渲染出11个,多出了一个是为了滚动内容会出现头部只有一部分的情况
  2. 然后可以根据holderscrollTop的高度可以计算出要从数组的哪一个位置过滤数据,然后将过滤后的数据循环展示到页面上

但是细心的朋友肯定就发现我们的滚动少了一个右边的滚动条,没有这个肯定是不行的

于是我们自画一个滚动条,并且加上自定义的拖动事件

image.png

  1. rc-virtual-list-scrollbar添加onMouseDown事件,阻止冒泡和默认事件
  2. rc-virtual-list-scrollbar-thumb添加onMouseDown事件 阻止冒泡和默认事件 将状态改为滚动(为了防止滚动期间list中的文字会被选中出现蓝色的背景) 绑定全局的mouseupmousemove事件

代码实现关键点

鼠标滚轴事件


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

最后,如果喜欢我的文章,可以给个赞👍或者关注➕都是对我最大的支持