使用 react 来做一些复杂的虚拟列表/表格, 还是可以感觉到掉帧, 也不平滑.
换成 solidjs 重新实现一遍功能. 非常的丝滑, 不愧是 性能接近原生js 的框架👍👍👍👍
npm install solidjs-use
"solidjs-use": "^2.3.0"
https://github.com/solidjs-use/solidjs-use
https://solidjs-use.github.io/solidjs-use/core/useVirtualList
传入自己的 array 数据, 并设置大概的 item 高度
let vList = useVirtualList(myDataList, {
itemHeight: 62,
});
虚拟表格, 两个 div 中放入 table , 在 tbody 中 循环显示 tr 即可
普通的列表, 则省略 table
<div
ref={vList.containerProps.ref}
style={vList.containerProps.style}
onScroll={vList.containerProps.onScroll}
class="h-full"
>
<div style={vList.wrapperProps().style}>
<Table
style={{
"table-layout": "fixed",
}}
class="w-full"
>
<thead>
<tr>
<th class="sticky top-0 bg-gray-800 text-white">id</th>
<th class="sticky top-0 bg-gray-800 text-white">名称</th>
<th class="sticky top-0 bg-gray-800 text-white">值</th>
</tr>
</thead>
<tbody>
<For each={vList.list()}>
{(listItem) => {
let item = listItem.data;
return (
<tr>
<td>{item.keyName}</td>
</tr>
);
}}
</For>
</tbody>
</Table>
</div>
小试牛刀, 用 solidjs 实现一个普通的虚拟列表,学习原理
仅供学习, 真实使用请选择 solidjs-use 或者 其他相关库
import {
batch,
createEffect,
createSignal,
For,
JSX,
MergeProps,
mergeProps,
on,
onMount,
} from "solid-js";
type DefaultProps<T, K extends keyof T> = MergeProps<[Required<Pick<T, K>>, T]>;
function useDefaultProps<T, K extends keyof T>(
props: T,
defaults: Required<Pick<T, K>>
): DefaultProps<T, K> {
// eslint-disable-next-line solid/reactivity
return mergeProps(defaults, props);
}
type OnParameters<T1, T2> = Parameters<typeof on<T1, T2>>;
let useEffectWatch = <T1, T2>(
a: OnParameters<T1, T2>[0],
b: OnParameters<T1, T2>[1],
c?: OnParameters<T1, T2>[2]
) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
createEffect(on(a, b, c));
};
let useEffectWatchDefer = <T1, T2>(
a: OnParameters<T1, T2>[0],
b: OnParameters<T1, T2>[1]
) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
createEffect(on(a, b, { defer: true }));
};
export function VirtualList<T>(inProps: {
data: T[];
rowHeight: number;
renderRow: (row: T) => JSX.Element;
overscanCount?: number;
style?: JSX.CSSProperties;
class?: string;
}) {
let props = useDefaultProps(inProps, {
overscanCount: 5,
});
let rootElement: HTMLDivElement;
const [height, setHeight] = createSignal<number>(0);
const [rowCount, setRowCount] = createSignal<number>(0);
const [offset, setOffset] = createSignal<number>(0);
const [start, setStart] = createSignal<number>(0);
const [end, setEnd] = createSignal<number>(0);
const [selection, setSelection] = createSignal<T[]>([]);
//确保容器的高度始终设置为根元素的偏移高度。
const resize = () => {
if (height() !== rootElement.offsetHeight) {
setHeight(rootElement.offsetHeight);
setRowCount(getRowCount());
}
};
// 获取渲染的第一个元素索引
const getStart = () => {
let ret = Number((offset() / props.rowHeight).toFixed(0));
ret = Math.max(0, ret - (ret % props.overscanCount));
//console.log("getStart:", ret);
return ret;
};
// 获取展示的行数
const getRowCount = () => {
let ret = Number((height() / props.rowHeight).toFixed(0)) + props.overscanCount;
//console.log("getRowCount:", ret);
return ret;
};
// 获取渲染的最后一个元素索引
const getEnd = () => {
let ret = start() + rowCount() + 1;
//console.log("getEnd:", ret);
return ret;
};
const tick = () => {
batch(() => {
setOffset(rootElement.scrollTop);
setStart(getStart());
setEnd(getEnd());
// 筛选渲染范围的数据
setSelection(props.data?.slice(start(), end()));
// console.log("tick:", {
// offset: offset(),
// start: start(),
// end: end(),
// selection: selection(),
// data: props.data,
// });
});
};
const handleScroll = () => {
//console.log(offset(), rootElement.scrollTop);
if (offset() != rootElement.scrollTop) {
tick();
}
};
onMount(() => {
resize();
tick();
rootElement?.addEventListener?.("resize", resize);
});
// 首次不触发, 交给 onMount
useEffectWatchDefer(
() => props.data, // 只监听 props.data
() => {
tick();
}
);
return (
<div
ref={(r) => (rootElement = r)}
style={{ overflow: "auto", ...props.style }}
class={props.class}
onScroll={handleScroll}
>
<div
style={{
position: "relative",
overflow: "hidden",
width: "100%",
"min-height": "100%",
height: `${(props.data?.length ?? 0) * props.rowHeight}px`,
}}
>
<div
style={{
position: "absolute",
top: `${start() * props.rowHeight}px`,
left: 0,
height: "100%",
width: "100%",
overflow: "visible",
}}
>
<For each={selection()}>
{(row) => {
return props.renderRow(row);
}}
</For>
</div>
</div>
</div>
);
}
使用
<VirtualList
style={{height:'100%'}}
data={dataList()!}
rowHeight={30}
renderRow={(row) => {
return (
<div style={{ height: "30px" }} >...</div>
)
}}
/>