乐趣区

关于前端:大家都能看得懂的源码之-ahooks-useVirtualList-封装虚拟滚动列表

本文是深入浅出 ahooks 源码系列文章的第十八篇,该系列已整顿成文档 - 地址。感觉还不错,给个 star 反对一下哈,Thanks。

简介

提供虚拟化列表能力的 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 值。

变量很多,能够联合下图,会比拟清晰了解:

<img width=”809″ alt=”image” src=”https://user-images.githubusercontent.com/20135760/185148673-1b02da0a-c6f4-4c6d-916e-ad0839f00185.png”>

代码如下:

// 计算范畴,由哪个开始,哪个完结
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();}
};

思考与总结

对于高度绝对比拟确定的状况,咱们做虚构滚动还是绝对简略的,但如果高度不确定呢?

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

本文已收录到集体博客中,欢送关注~

退出移动版