antd5 虚拟列表原理(rc-virtual-list)

github:https://github.com/react-component/virtual-list

rc-virtual-list 版本 3.11.4(2024-02-01)

版本:virtual-list-3.11.4

Development

bash 复制代码
npm install
npm start
open http://localhost:8000/

List 组件接收 Props

Prop Description Type Default
children Render props of item (item, index, props) => ReactElement -
component Customize List dom element string | Component div
data Data list Array -
disabled Disable scroll check. Usually used on animation control boolean false
height List height number -
itemHeight Item minium height number -
itemKey Match key with item string -
styles style { horizontalScrollBar?: React.CSSProperties; horizontalScrollBarThumb?: React.CSSProperties; verticalScrollBar?: React.CSSProperties; verticalScrollBarThumb?: React.CSSProperties; } -

组件解析

ts 复制代码
import ResizeObserver from "rc-resize-observer";

const onHolderResize: ResizeObserverProps["onResize"] = (sizeInfo) => {
  console.log("sizeInfo", sizeInfo);

  setSize({
    width: sizeInfo.width || sizeInfo.offsetWidth,
    height: sizeInfo.height || sizeInfo.offsetHeight,
  });
};

// 用于监听dom节点resize时返回dom节点信息
<ResizeObserver onResize={onHolderResize}></ResizeObserver>;

打印的 sizeInfo

js 复制代码
{
  height: 200,//可视区高度
  offsetHeight: 200,
  offsetWidth: 606,
  width: 606,//可视区宽度
}
ts 复制代码
 //component: Component = 'div',
// Component默认是div标签 ,className为rc-virtual-list-holder, 是虚拟列表的可视化区域
<Component
    className={`${prefixCls}-holder`}
    style={componentStyle}
    ref={componentRef}
    onScroll={onFallbackScroll}
    onMouseEnter={delayHideScrollBar}
  >

componentStyle 计算,是一个 styles 对象 React.CSSProperties

ts 复制代码
const ScrollStyle: React.CSSProperties = {
  overflowY: "auto",
  overflowAnchor: "none",
};

// useVirtual: 是否虚拟列表(属性virtual为true 并且height和itemHeight有值)
const useVirtual = !!(virtual !== false && height && itemHeight);

let componentStyle: React.CSSProperties = null;
if (height) {
  componentStyle = {
    [fullHeight ? "height" : "maxHeight"]: height,
    ...ScrollStyle,
  };

  if (useVirtual) {
    componentStyle.overflowY = "hidden";

    if (scrollWidth) {
      componentStyle.overflowX = "hidden";
    }

    if (scrollMoving) {
      componentStyle.pointerEvents = "none";
    }
  }
}

overflow-anchor CSS 属性提供一种退出浏览器滚动锚定行为的方法,该行为会调整滚动位置以最大程度地减少内容偏移。

默认情况下,在任何支持滚动锚定行为的浏览器中都将其启用。因此,仅当你在文档或文档的一部分中遇到滚动锚定问题并且需要关闭行为时,才通常需要更改此属性的值。

内容组件

import Filler from './Filler';

ts 复制代码
<Filler
  prefixCls={prefixCls}
  height={scrollHeight}
  offsetX={offsetLeft}
  offsetY={fillerOffset}
  scrollWidth={scrollWidth}
  onInnerResize={collectHeight}
  ref={fillerInnerRef}
  innerProps={innerProps}
  rtl={isRTL}
  extra={extraContent}
>
  {listChildren}
</Filler>

Filler 组件

ts 复制代码
<div style={outerStyle}>
  <ResizeObserver
    onResize={({ offsetHeight }) => {
      if (offsetHeight && onInnerResize) {
        onInnerResize();
      }
    }}
  >
    <div
      style={innerStyle}
      className={classNames({
        [`${prefixCls}-holder-inner`]: prefixCls,
      })}
      ref={ref}
      {...innerProps}
    >
      {children}
      {extra}
    </div>
  </ResizeObserver>
</div>

demo 查看渲染内容

outStyle 计算:

ts 复制代码
let outerStyle: React.CSSProperties = {};

if (offsetY !== undefined) {
  // Not set `width` since this will break `sticky: right`
  outerStyle = {
    height,
    position: "relative",
    overflow: "hidden",
  };
}

innerStyle 计算

ts 复制代码
let innerStyle: React.CSSProperties = {
  display: "flex",
  flexDirection: "column",
};
if (offsetY !== undefined) {
  innerStyle = {
    ...innerStyle,
    transform: `translateY(${offsetY}px)`,
    [rtl ? "marginRight" : "marginLeft"]: -offsetX,
    position: "absolute",
    left: 0,
    right: 0,
    top: 0,
  };
}

可以看到最终渲染的元素,有下面几个容器组成:

列表容器:rc-virtual-list

列表内容容器:rc-virtual-list-holder

要点:

Component 组件,默认 div:固定高度,超出部分隐藏,最终也是通过控制该容器的滚动高度来达到元素滚动的目的

div(outStyle):高度为所有列表内容都渲染出来的高度,这里是为了撑开父元素,实现父元素的滚动

渲染列表容器:rc-virtual-list-holder-inner

单个列表内容:item

listChildren

ts 复制代码
const listChildren = useChildren(
  mergedData, //列表数据
  start, //渲染第一个元素的索引
  end, //渲染最后一个元素的索引
  scrollWidth,
  setInstanceRef, //获取元素
  children,
  sharedConfig
);

useChildren 主要是进行 list 列表的渲染,而在渲染列表时,又用 Item 组件进行了一层包裹.

ts 复制代码
export default function useChildren<T>(
  list: T[],
  startIndex: number,
  endIndex: number,
  scrollWidth: number,
  setNodeRef: (item: T, element: HTMLElement) => void,
  renderFunc: RenderFunc<T>,
  { getKey }: SharedConfig<T>
) {
  return list.slice(startIndex, endIndex + 1).map((item, index) => {
    const eleIndex = startIndex + index;
    const node = renderFunc(item, eleIndex, {
      style: {
        width: scrollWidth,
      },
    }) as React.ReactElement;

    const key = getKey(item);
    return (
      <Item key={key} setRef={(ele) => setNodeRef(item, ele)}>
        {node}
      </Item>
    );
  });
}

Item 组件

用 Item 组件包裹了外部传入的列表元素的 JSXElement

ts 复制代码
export interface ItemProps {
  children: React.ReactElement;
  setRef: (element: HTMLElement) => void;
}

export function Item({ children, setRef }: ItemProps) {
  const refFunc = React.useCallback((node) => {
    setRef(node);
  }, []);

  return React.cloneElement(children, {
    ref: refFunc,
  });
}

经过这么一层包装,当通过 ref 获取子节点时,将会调用 refFunc -> setRef -> setInstanceRef。这也是为什么当元素高度可变时需要用 React.forwardRef 进行列表元素的包裹

滚动条组件

ts 复制代码
<ScrollBar
  ref={verticalScrollBarRef}
  prefixCls={prefixCls}
  scrollOffset={offsetTop}
  scrollRange={scrollHeight}
  rtl={isRTL}
  onScroll={onScrollBar} //滚动事件
  onStartMove={onScrollbarStartMove} //开始滚动事件
  onStopMove={onScrollbarStopMove} //滚动结束事件
  spinSize={verticalScrollBarSpinSize}
  containerSize={size.height}
  style={styles?.verticalScrollBar}
  thumbStyle={styles?.verticalScrollBarThumb}
/>

ScrollBar 渲染

js 复制代码
<div
  ref={scrollbarRef}
  className={classNames(scrollbarPrefixCls, {
    [`${scrollbarPrefixCls}-horizontal`]: horizontal,
    [`${scrollbarPrefixCls}-vertical`]: !horizontal,
    [`${scrollbarPrefixCls}-visible`]: visible,
  })}
  style={{ ...containerStyle, ...style }}
  onMouseDown={onContainerMouseDown}
  onMouseMove={delayHidden}
>
  <div
    ref={thumbRef}
    className={classNames(`${scrollbarPrefixCls}-thumb`, {
      [`${scrollbarPrefixCls}-thumb-moving`]: dragging,
    })}
    style={{ ...thumbStyle, ...propsThumbStyle }}
    onMouseDown={onThumbMouseDown}
  />
</div>

通过滚动条组件滚动事件

js 复制代码
//newScrollOffset 滚动的距离,horizontal是否水平滚动方向
function onScrollBar(newScrollOffset: number, horizontal?: boolean) {
  const newOffset = newScrollOffset;
  if (horizontal) {
    flushSync(() => {
      setOffsetLeft(newOffset);
    });
    triggerScroll();
  } else {
    syncScrollTop(newOffset);
  }
}

滚动条开始滚动事件和滚动结束事件

js 复制代码
// 滚动开始事件
const onScrollbarStartMove = () => {
  console.log("----start-----");

  setScrollMoving(true);
};

//滚动结束事件
const onScrollbarStopMove = () => {
  console.log("-----end");

  setScrollMoving(false);
};

注意点

  • 如果子项存在动态高度或者高度不统一的情况,需要使用 React.forwardRef 转发 ref 给子 DOM 元素。
  • 列表项之间不要存在上下间距( margin-top 、 margin-bottom )。
    以上两点如果没有做到,调用组件的 scrollTo(scrollConfig) 方法进行滚动时都会导致滚动位置异常
相关推荐
XuanRanDev16 分钟前
【每日一题】LeetCode - 三数之和
数据结构·算法·leetcode·1024程序员节
代码猪猪傻瓜coding18 分钟前
力扣1 两数之和
数据结构·算法·leetcode
南宫生2 小时前
贪心算法习题其三【力扣】【算法学习day.20】
java·数据结构·学习·算法·leetcode·贪心算法
weixin_432702262 小时前
代码随想录算法训练营第五十五天|图论理论基础
数据结构·python·算法·深度优先·图论
passer__jw7673 小时前
【LeetCode】【算法】283. 移动零
数据结构·算法·leetcode
爱吃生蚝的于勒4 小时前
深入学习指针(5)!!!!!!!!!!!!!!!
c语言·开发语言·数据结构·学习·计算机网络·算法
羊小猪~~4 小时前
数据结构C语言描述2(图文结合)--有头单链表,无头单链表(两种方法),链表反转、有序链表构建、排序等操作,考研可看
c语言·数据结构·c++·考研·算法·链表·visual studio
脉牛杂德5 小时前
多项式加法——C语言
数据结构·c++·算法
一直学习永不止步5 小时前
LeetCode题练习与总结:赎金信--383
java·数据结构·算法·leetcode·字符串·哈希表·计数
wheeldown13 小时前
【数据结构】选择排序
数据结构·算法·排序算法