简介

提供虚拟化列表能力的 Hook,用于解决展示海量数据渲染时首屏渲染缓慢和滚动卡顿问题。

详情可见官网,文章源代码可以点击这里。

实现原理

其实现原理监听外部容器的 scroll 事件以及其 size 发生变化的时候,触发计算逻辑算出内部容器的高度和 marginTop 值。

具体实现

其监听滚动逻辑如下:

// 当外部容器的 size 发生变化的时候,触发计算逻辑
useEffect(() => {
  if (!size?.width || !size?.height) {
    return;
  }
  // 重新计算逻辑
  calculateRange();
}, [size?.width, size?.height, list]);
// 监听外部容器的 scroll 事件
useEventListener(
  'scroll',
  e => {
    // 如果是直接跳转,则不需要重新计算
    if (scrollTriggerByScrollToFunc.current) {
      scrollTriggerByScrollToFunc.current = false;
      return;
    }
    e.preventDefault();
    // 计算
    calculateRange();
  },
  {
    // 外部容器
    target: containerTarget,
  },
);

其中 calculateRange 非常重要,它基本实现了虚拟滚动的主流程逻辑,其主要做了以下的事情:

  • 获取到整个内部容器的高度 totalHeight。
  • 根据外部容器的 scrollTop 算出已经“滚过”多少项,值为 offset。
  • 根据外部容器高度以及当前的开始索引,获取到外部容器能承载的个数 visibleCount。
  • 并根据 overscan(视区上、下额外展示的 DOM 节点数量)计算出开始索引(start)和(end)。
  • 根据开始索引获取到其距离最开始的距离(offsetTop)。
  • 最后根据 offsetTop 和 totalHeight 设置内部容器的高度和 marginTop 值。

变量很多,可以结合下图,会比较清晰理解:

代码如下:

// 计算范围,由哪个开始,哪个结束
const calculateRange = () => {
  // 获取外部和内部容器
  // 外部容器
  const container = getTargetElement(containerTarget);
  // 内部容器
  const wrapper = getTargetElement(wrapperTarget);
  if (container && wrapper) {
    const {
      // 滚动距离顶部的距离。设置或获取位于对象最顶端和窗口中可见内容的最顶端之间的距离
      scrollTop,
      // 内容可视区域的高度
      clientHeight,
    } = container;
    // 根据外部容器的 scrollTop 算出已经“滚过”多少项
    const offset = getOffset(scrollTop);
    // 可视区域的 DOM 个数
    const visibleCount = getVisibleCount(clientHeight, offset);
    // 开始的下标
    const start = Math.max(0, offset - overscan);
    // 结束的下标
    const end = Math.min(list.length, offset   visibleCount   overscan);
    // 获取上方高度
    const offsetTop = getDistanceTop(start);
    // 设置内部容器的高度,总的高度 - 上方高度
    // @ts-ignore
    wrapper.style.height = totalHeight - offsetTop   'px';
    // margin top 为上方高度
    // @ts-ignore
    wrapper.style.marginTop = offsetTop   'px';
    // 设置最后显示的 List
    setTargetList(
      list.slice(start, end).map((ele, index) => ({
        data: ele,
        index: index   start,
      })),
    );
  }
};

其它就是这个函数的辅助函数了,包括:

  • 根据外部容器以及内部每一项的高度,计算出可视区域内的数量:
// 根据外部容器以及内部每一项的高度,计算出可视区域内的数量
const getVisibleCount = (containerHeight: number, fromIndex: number) => {
  // 知道每一行的高度 - number 类型,则根据容器计算
  if (isNumber(itemHeightRef.current)) {
    return Math.ceil(containerHeight / itemHeightRef.current);
  }
  // 动态指定每个元素的高度情况
  let sum = 0;
  let endIndex = 0;
  for (let i = fromIndex; i < list.length; i  ) {
    // 计算每一个 Item 的高度
    const height = itemHeightRef.current(i, list[i]);
    sum  = height;
    endIndex = i;
    // 大于容器宽度的时候,停止
    if (sum >= containerHeight) {
      break;
    }
  }
  // 最后一个的下标减去开始一个的下标
  return endIndex - fromIndex;
};
  • 根据 scrollTop 计算上面有多少个 DOM 节点:
// 根据 scrollTop 计算上面有多少个 DOM 节点
const getOffset = (scrollTop: number) => {
  // 每一项固定高度
  if (isNumber(itemHeightRef.current)) {
    return Math.floor(scrollTop / itemHeightRef.current)   1;
  }
  // 动态指定每个元素的高度情况
  let sum = 0;
  let offset = 0;
  // 从 0 开始
  for (let i = 0; i < list.length; i  ) {
    const height = itemHeightRef.current(i, list[i]);
    sum  = height;
    if (sum >= scrollTop) {
      offset = i;
      break;
    }
  }
  // 满足要求的最后一个   1
  return offset   1;
};
  • 获取上部高度:
// 获取上部高度
const getDistanceTop = (index: number) => {
  // 每一项高度相同
  if (isNumber(itemHeightRef.current)) {
    const height = index * itemHeightRef.current;
    return height;
  }
  // 动态指定每个元素的高度情况,则 itemHeightRef.current 为函数
  const height = list
    .slice(0, index)
    // reduce 计算总和
    // @ts-ignore
    .reduce((sum, _, i) => sum   itemHeightRef.current(i, list[index]), 0);
  return height;
};
  • 计算总的高度:
// 计算总的高度
const totalHeight = useMemo(() => {
  // 每一项高度相同
  if (isNumber(itemHeightRef.current)) {
    return list.length * itemHeightRef.current;
  }
  // 动态指定每个元素的高度情况
  // @ts-ignore
  return list.reduce(
    (sum, _, index) => sum   itemHeightRef.current(index, list[index]),
    0,
  );
}, [list]);

最后暴露一个滚动到指定的 index 的函数,其主要是计算出该 index 距离顶部的高度 scrollTop,设置给外部容器。并触发 calculateRange 函数。

// 滚动到指定的 index
const scrollTo = (index: number) => {
  const container = getTargetElement(containerTarget);
  if (container) {
    scrollTriggerByScrollToFunc.current = true;
    // 滚动
    container.scrollTop = getDistanceTop(index);
    calculateRange();
  }
};

思考总结

对于高度相对比较确定的情况,我们做虚拟滚动还是相对简单的,但假如高度不确定呢?

或者换另外一个角度,当我们的滚动不是纵向的时候,而是横向,该如何处理呢?

以上就是ahooks useVirtualList 封装虚拟滚动列表的详细内容,更多关于ahooks useVirtualList封装的资料请关注Devmax其它相关文章!

ahooks useVirtualList 封装虚拟滚动列表的更多相关文章

  1. 用vue3封装一个符合思维且简单实用的弹出层

    最近新项目中需要一个弹窗组件,所以我就做了一个,下面这篇文章主要给大家介绍了关于如何利用vue3封装一个符合思维且简单实用的弹出层,文中通过实例代码介绍的非常详细,需要的朋友可以参考下

  2. Node.js中对通用模块的封装方法

    这篇文章主要介绍了Node.js中对通用模块的封装方法,封装方法参考了Underscore.js的实现,需要的朋友可以参考下

  3. java封装及四种权限修饰符详解

    这篇文章主要介绍了java封装及四种权限修饰符详解,对属性进行封装,使用户不能直接输入数据,我们需要避免用户再使用"对象.属性"的方式对属性进行赋值

  4. 在vue中如何封装G2图表

    这篇文章主要介绍了在vue中如何封装G2图表,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

  5. JS样式获取的封装方法实例详解

    这篇文章主要介绍了JS样式获取的封装方法,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  6. Ajax基础教程之封装(三)

    这篇文章给大家介绍ajax基础教程之封装的知识,本文通过实例给大家详细介绍,非常不错,感兴趣的朋友一起学习吧

  7. JavaScript函数封装的示例详解

    这篇文章主要通过动画的示例来为大家详细介绍一下JavaScript的函数封装,文中的示例代码讲解详细,感兴趣的小伙伴可以学习一下

  8. Flutter Dio二次封装的实现

    这篇文章主要介绍了Flutter Dio二次封装的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

  9. React路由封装的实现浅析

    路由是React项目中相当重要的概念,对于功能较为复杂的网页来说,必然会涉及到不同功能间的页面跳转,本篇文章将对React官方维护的路由库React-Router-Dom的使用和常用组件进行讲解,同时对路由组件传递param参数的方式进行讲解,希望对各位读者有所参考

  10. vue日常开发基础Axios网络库封装

    这篇文章主要为大家介绍了vue日常开发基础Axios网络库封装示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

随机推荐

  1. js中‘!.’是什么意思

  2. Vue如何指定不编译的文件夹和favicon.ico

    这篇文章主要介绍了Vue如何指定不编译的文件夹和favicon.ico,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

  3. 基于JavaScript编写一个图片转PDF转换器

    本文为大家介绍了一个简单的 JavaScript 项目,可以将图片转换为 PDF 文件。你可以从本地选择任何一张图片,只需点击一下即可将其转换为 PDF 文件,感兴趣的可以动手尝试一下

  4. jquery点赞功能实现代码 点个赞吧!

    点赞功能很多地方都会出现,如何实现爱心点赞功能,这篇文章主要为大家详细介绍了jquery点赞功能实现代码,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  5. AngularJs上传前预览图片的实例代码

    使用AngularJs进行开发,在项目中,经常会遇到上传图片后,需在一旁预览图片内容,怎么实现这样的功能呢?今天小编给大家分享AugularJs上传前预览图片的实现代码,需要的朋友参考下吧

  6. JavaScript面向对象编程入门教程

    这篇文章主要介绍了JavaScript面向对象编程的相关概念,例如类、对象、属性、方法等面向对象的术语,并以实例讲解各种术语的使用,非常好的一篇面向对象入门教程,其它语言也可以参考哦

  7. jQuery中的通配符选择器使用总结

    通配符在控制input标签时相当好用,这里简单进行了jQuery中的通配符选择器使用总结,需要的朋友可以参考下

  8. javascript 动态调整图片尺寸实现代码

    在自己的网站上更新文章时一个比较常见的问题是:文章插图太宽,使整个网页都变形了。如果对每个插图都先进行缩放再插入的话,太麻烦了。

  9. jquery ajaxfileupload异步上传插件

    这篇文章主要为大家详细介绍了jquery ajaxfileupload异步上传插件,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  10. React学习之受控组件与数据共享实例分析

    这篇文章主要介绍了React学习之受控组件与数据共享,结合实例形式分析了React受控组件与组件间数据共享相关原理与使用技巧,需要的朋友可以参考下

返回
顶部