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) 方法进行滚动时都会导致滚动位置异常