• 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

    npm install
    npm start
    open http://localhost:8000/
    
    • 1
    • 2
    • 3

    在这里插入图片描述

    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>;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    打印的 sizeInfo

    {
      height: 200,//可视区高度
      offsetHeight: 200,
      offsetWidth: 606,
      width: 606,//可视区宽度
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
     //component: Component = 'div',
    // Component默认是div标签 ,className为rc-virtual-list-holder, 是虚拟列表的可视化区域
    <Component
        className={`${prefixCls}-holder`}
        style={componentStyle}
        ref={componentRef}
        onScroll={onFallbackScroll}
        onMouseEnter={delayHideScrollBar}
      >
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    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";
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    demo 查看渲染内容

    在这里插入图片描述

    outStyle 计算:

    let outerStyle: React.CSSProperties = {};
    
    if (offsetY !== undefined) {
      // Not set `width` since this will break `sticky: right`
      outerStyle = {
        height,
        position: "relative",
        overflow: "hidden",
      };
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    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,
      };
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

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

    列表容器: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
    );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    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>
        );
      });
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    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,
      });
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    经过这么一层包装,当通过 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}
    />
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    通过滚动条组件滚动事件

    //newScrollOffset 滚动的距离,horizontal是否水平滚动方向
    function onScrollBar(newScrollOffset: number, horizontal?: boolean) {
      const newOffset = newScrollOffset;
      if (horizontal) {
        flushSync(() => {
          setOffsetLeft(newOffset);
        });
        triggerScroll();
      } else {
        syncScrollTop(newOffset);
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

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

    // 滚动开始事件
    const onScrollbarStartMove = () => {
      console.log("----start-----");
    
      setScrollMoving(true);
    };
    
    //滚动结束事件
    const onScrollbarStopMove = () => {
      console.log("-----end");
    
      setScrollMoving(false);
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    注意点

    • 如果子项存在动态高度或者高度不统一的情况,需要使用 React.forwardRef 转发 ref 给子 DOM 元素。
    • 列表项之间不要存在上下间距( margin-top 、 margin-bottom )。
      以上两点如果没有做到,调用组件的 scrollTo(scrollConfig) 方法进行滚动时都会导致滚动位置异常
  • 相关阅读:
    MIT6.824-lab3A-Key/value service without snapshots(基本的KV服务)
    c#特性 --- 委托(delegate)与 异常处理
    华为云智能云接入ICA,助力企业轻松上云
    整车电子电器架构和自动驾驶架构的区别
    Vue2和Vue3的区别——实例创建、响应式数据代理、v-for和v-if优先级、生命周期
    Java实现经纬度坐标转换(GPS-高德)
    TrOCR – 基于 Transformer 的 OCR 入门
    测试路由器
    TypeError: list indices must be integers or slices, not tuple 错误原因解决方法
    python-素数对
  • 原文地址:https://blog.csdn.net/weixin_40119412/article/details/136686981