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

本文介绍了rc-virtual-list3.11.4版本的开发细节,涉及组件接收的Props,如高度、滚动控制和自定义渲染,以及如何处理动态高度和滚动条的实现。特别强调了使用React.forwardRef处理高度变化元素的重要性。
摘要由CSDN通过智能技术生成

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

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

在这里插入图片描述

Development

npm install
npm start
open http://localhost:8000/

在这里插入图片描述

List 组件接收 Props

PropDescriptionTypeDefault
childrenRender props of item(item, index, props) => ReactElement-
componentCustomize List dom elementstring | Componentdiv
dataData listArray-
disabledDisable scroll check. Usually used on animation controlbooleanfalse
heightList heightnumber-
itemHeightItem minium heightnumber-
itemKeyMatch key with itemstring-
stylesstyle{ horizontalScrollBar?: React.CSSProperties; horizontalScrollBarThumb?: React.CSSProperties; verticalScrollBar?: React.CSSProperties; verticalScrollBarThumb?: React.CSSProperties; }-

组件解析

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

{
  height: 200,//可视区高度
  offsetHeight: 200,
  offsetWidth: 606,
  width: 606,//可视区宽度
}
 //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

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’;

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

Filler 组件

<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 计算:

let outerStyle: React.CSSProperties = {};

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

innerStyle 计算

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

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

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

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

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 进行列表元素的包裹

滚动条组件

<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 渲染

<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>

通过滚动条组件滚动事件

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

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

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

  setScrollMoving(true);
};

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

  setScrollMoving(false);
};

注意点

  • 如果子项存在动态高度或者高度不统一的情况,需要使用 React.forwardRef 转发 ref 给子 DOM 元素。
  • 列表项之间不要存在上下间距( margin-top 、 margin-bottom )。
    以上两点如果没有做到,调用组件的 scrollTo(scrollConfig) 方法进行滚动时都会导致滚动位置异常
  • 15
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值