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) 方法进行滚动时都会导致滚动位置异常
相关推荐
菜鸡中的奋斗鸡→挣扎鸡1 小时前
滑动窗口 + 算法复习
数据结构·算法
axxy20002 小时前
leetcode之hot100---240搜索二维矩阵II(C++)
数据结构·算法
Uu_05kkq3 小时前
【C语言1】C语言常见概念(总结复习篇)——库函数、ASCII码、转义字符
c语言·数据结构·算法
1nullptr5 小时前
三次翻转实现数组元素的旋转
数据结构
TT哇5 小时前
【数据结构练习题】链表与LinkedList
java·数据结构·链表
A懿轩A6 小时前
C/C++ 数据结构与算法【栈和队列】 栈+队列详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·考研·算法·栈和队列
1 9 J7 小时前
数据结构 C/C++(实验五:图)
c语言·数据结构·c++·学习·算法
汝即来归7 小时前
选择排序和冒泡排序;MySQL架构
数据结构·算法·排序算法
aaasssdddd9610 小时前
C++的封装(十四):《设计模式》这本书
数据结构·c++·设计模式
芳菲菲其弥章10 小时前
数据结构经典算法总复习(下卷)
数据结构·算法