yh-react-virtuallist
Version:
yh-react-virtuallist 虚拟列表组件
211 lines (210 loc) • 8.55 kB
JavaScript
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react";
import { unstable_batchedUpdates } from "react-dom";
function arrayResolve(value, isArrayFunc, notArrayFunc) {
if (Array.isArray(value)) {
return isArrayFunc(value);
}
else {
console.error("you must pass array children ");
return notArrayFunc();
}
}
export function VirtualList(props) {
const { scrollDom, referItemHeight, renderNumber, children } = props, rest = __rest(props, ["scrollDom", "referItemHeight", "renderNumber", "children"]);
const [scrollDomParams, setScrollDomParams] = useState({
width: 0,
height: 0,
top: 0,
left: 0,
});
useEffect(() => {
if (props.scrollDom.current) {
const rect = props.scrollDom.current.getBoundingClientRect();
setScrollDomParams({
width: rect.width,
height: rect.height,
left: rect.left,
top: rect.top,
});
}
}, [props.scrollDom]);
const [childrenWrapParams, setChildrenWrapParams] = useState({
width: 0,
height: 0,
top: 0,
left: 0,
});
const ref = useRef(null);
useEffect(() => {
if (ref.current) {
const rect = ref.current.getBoundingClientRect();
setChildrenWrapParams({
width: rect.width,
height: rect.height,
left: rect.left,
top: rect.top,
});
}
}, []);
const wrapperToScrollDomDistance = useMemo(() => {
return childrenWrapParams.top - scrollDomParams.top;
}, [childrenWrapParams.top, scrollDomParams.top]);
//为每个元素建立高度
const cache = useMemo(() => {
return arrayResolve(props.children, (val) => {
return val.reduce((prev, next, index) => {
prev[index] = props.referItemHeight;
return prev;
}, {});
}, () => { });
}, [props.children, props.referItemHeight]);
const [mockHeight, setMockHeight] = useState(() => {
return Object.values(cache).reduce((prev, next) => prev + next, 0);
});
useEffect(() => {
setMockHeight(Object.values(cache).reduce((prev, next) => prev + next, 0) -
scrollDomParams.height);
}, [cache, scrollDomParams.height]);
const refData = useMemo(() => {
return {};
}, []);
const cloneChildren = useMemo(() => {
return arrayResolve(props.children, (val) => {
return val.map((v, i) => {
const oprops = v.props;
return React.cloneElement(v, Object.assign(Object.assign({}, oprops), { ref: (node) => {
refData[i] = node;
} }));
});
}, () => []);
}, [props.children, refData]);
const [renderChildren, setRenderChildren] = useState(
//一开始,需要返回对应截取的元素
() => {
return arrayResolve(cloneChildren, (val) => val.slice(0, props.renderNumber), () => []);
});
//初次返回,我们进行修正cache //初次渲染 0- props.renderNumber
useLayoutEffect(() => {
if (
//如果0存在,说明已经显示了,
refData[0]) {
//map rendernumber
new Array(props.renderNumber).fill(1).forEach((x, y) => {
if (refData[y]) {
const height = refData[y].getBoundingClientRect().height || cache[y];
cache[y] = height;
}
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const [viewPortY, setViewPortY] = useState(0);
const current = useMemo(() => {
return {
//以start为界。每次删除前面的,加入后面的,并且修正cache
start: props.renderNumber,
};
}, [props.renderNumber]);
const maxY = useMemo(() => {
//最大值等于mock高减去一屏幕高度
return mockHeight - scrollDomParams.height - scrollDomParams.height;
}, [mockHeight, scrollDomParams.height]);
useEffect(() => {
let fn;
let timer;
if (props.scrollDom.current) {
fn = (e) => {
const target = e.target;
const scroll = target.scrollTop - scrollDomParams.height;
//根据scroll的高度判断滚到第几个位置
let sum = 0;
let sindex = 0;
Object.values(cache).some((v, i) => {
sum = sum + v;
if (sum > scroll) {
sindex = i + 1; //除非一个元素占一屏幕,否则一般不会有bug
return true;
}
return false;
});
const remain = props.renderNumber + sindex + props.renderNumber >
cloneChildren.length
? cloneChildren.length
: props.renderNumber + props.renderNumber + sindex;
const start = current.start;
if (start < remain && start < cloneChildren.length) {
timer = window.setTimeout(() => {
for (let i = start; i < remain; i++) {
if (refData[i]) {
const height = refData[i].getBoundingClientRect().height ||
cache[i];
cache[i] = height;
}
}
setMockHeight(Object.values(cache).reduce((prev, next) => prev + next, 0) - scrollDomParams.height);
current.start = remain;
//删除start之前的dom
for (let i = 0; i < start; i++) {
if (refData[i]) {
delete refData[i];
}
}
});
}
//减去一屏幕高度
let Y = scroll -
wrapperToScrollDomDistance -
scrollDomParams.height;
if (Y < 0) {
Y = 0;
//最后的scroll 需要减去一屏幕高度
}
else if (Y >= maxY) {
Y = maxY;
}
unstable_batchedUpdates(() => {
setRenderChildren(cloneChildren.slice(0 + sindex, remain));
setViewPortY(Y);
});
};
props.scrollDom.current.addEventListener("scroll", fn);
}
return () => {
if (props.scrollDom.current) {
//解绑非常重要,否则渲再次出现渲染会出严重问题
props.scrollDom.current.removeEventListener("scroll", fn);
}
window.clearTimeout(timer);
};
}, [
cache,
cloneChildren,
current,
maxY,
props.referItemHeight,
props.renderNumber,
props.scrollDom,
refData,
scrollDomParams.height,
wrapperToScrollDomDistance,
]);
return (React.createElement("div", Object.assign({ className: "yh-supervirtuallist", style: { display: "flex", position: "relative", width: "100%" } }, rest),
React.createElement("div", { style: { height: mockHeight } }),
React.createElement("div", { ref: ref, style: {
position: "absolute",
transform: `translate3d(0px, ${viewPortY}px, 0px)`,
width: "100%",
} }, renderChildren)));
}
export default VirtualList;