@jason12306/react-async-virtual-scroll
Version:
High-performance asynchronous virtual scroll component for react with dynamic height support and optimized rendering for large datasets
374 lines (373 loc) • 13.4 kB
JavaScript
import { jsxs as fe, jsx as D } from "react/jsx-runtime";
import me, { useState as E, useRef as M, useMemo as B, useEffect as O } from "react";
import { clone as F } from "lodash-es";
class xe {
uniqueKey;
bucket;
updateAt;
constructor(t = "id") {
this.uniqueKey = t, this.bucket = /* @__PURE__ */ new Map(), this.updateAt = 0;
}
/**
* 用指定高度填充所有数据项
*/
fill(t, i) {
this.bucket.clear();
for (const d of t) {
const l = String(d[this.uniqueKey]);
this.bucket.set(l, {
height: i,
key: l
});
}
this.updateAt = Date.now();
}
/**
* 获取指定 key 的缓存高度对象
*/
get(t) {
return this.bucket.get(String(t));
}
/**
* 获取所有缓存的高度对象
*/
getAll() {
return [...this.bucket.values()];
}
/**
* 设置指定 key 的高度
*/
set(t, i) {
const d = String(t);
if (this.bucket.has(d)) {
const l = this.bucket.get(d);
this.bucket.set(d, {
...l,
height: i
});
} else
this.bucket.set(d, {
key: d,
height: i
});
this.updateAt = Date.now();
}
/**
* 获取所有缓存高度的总和
*/
total() {
return this.getAll().map((t) => t.height).reduce((t, i) => t + i, 0);
}
/**
* 根据一组 id 获取这些项的高度总和
*/
getHeightByIds(t) {
const i = [];
for (const d of t)
i.push(this.get(String(d))?.height || 0);
return i.reduce((d, l) => d + l, 0);
}
/**
* 判断是否存在指定 key 的缓存
*/
has(t) {
return this.bucket.has(t);
}
/**
* 遍历所有缓存项
*/
forEach(t) {
this.getAll().forEach((i) => {
t(i);
});
}
/**
* 清除指定 key 的缓存
*/
clearByKeys(t) {
for (const i of t)
this.bucket.delete(String(i));
this.updateAt = Date.now();
}
/**
* 清空所有缓存
*/
empty() {
this.bucket.clear();
}
}
function ee(u) {
let t = 0;
for (const i of u)
i.$_index = t, t++;
}
function pe(u) {
if (u.parentNode && u.parentNode.classList.contains(
"async-virtual-scroll-wrapper"
))
return;
const t = document.createElement("div");
t.className = "async-virtual-scroll-wrapper", t.style.position = "relative", u.parentNode.insertBefore(t, u), t.appendChild(u);
}
const Ie = '<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M512 74.666667c-17.066667 0-32 14.933333-32 32v149.333333c0 17.066667 14.933333 32 32 32s32-14.933333 32-32V106.666667c0-17.066667-14.933333-32-32-32zM693.333333 362.666667c8.533333 0 17.066667-2.133333 23.466667-8.533334l104.533333-104.533333c12.8-12.8 12.8-32 0-44.8-12.8-12.8-32-12.8-44.8 0l-104.533333 104.533333c-12.8 12.8-12.8 32 0 44.8 4.266667 6.4 12.8 8.533333 21.333333 8.533334zM917.333333 480h-149.333333c-17.066667 0-32 14.933333-32 32s14.933333 32 32 32h149.333333c17.066667 0 32-14.933333 32-32s-14.933333-32-32-32zM714.666667 669.866667c-12.8-12.8-32-12.8-44.8 0s-12.8 32 0 44.8l104.533333 104.533333c6.4 6.4 14.933333 8.533333 23.466667 8.533333s17.066667-2.133333 23.466666-8.533333c12.8-12.8 12.8-32 0-44.8l-106.666666-104.533333zM512 736c-17.066667 0-32 14.933333-32 32v149.333333c0 17.066667 14.933333 32 32 32s32-14.933333 32-32v-149.333333c0-17.066667-14.933333-32-32-32zM309.333333 669.866667l-104.533333 104.533333c-12.8 12.8-12.8 32 0 44.8 6.4 6.4 14.933333 8.533333 23.466667 8.533333s17.066667-2.133333 23.466666-8.533333l104.533334-104.533333c12.8-12.8 12.8-32 0-44.8s-36.266667-12.8-46.933334 0zM288 512c0-17.066667-14.933333-32-32-32H106.666667c-17.066667 0-32 14.933333-32 32s14.933333 32 32 32h149.333333c17.066667 0 32-14.933333 32-32zM247.466667 202.666667c-12.8-12.8-32-12.8-44.8 0-12.8 12.8-12.8 32 0 44.8l104.533333 104.533333c6.4 6.4 14.933333 8.533333 23.466667 8.533333s17.066667-2.133333 23.466666-8.533333c12.8-12.8 12.8-32 0-44.8l-106.666666-104.533333z" fill="#666666"></path></svg>';
function te(u) {
const [t, i] = E(u), [d, l] = E(null);
return u !== t && (l(t), i(u)), d;
}
const ye = ({
value: u = !1,
ban: t = !1,
itemSize: i,
minItemSize: d,
data: l = [],
buffer: x = [200, 200],
keyField: p = "id",
dataUid: ne = "data-uid",
viewNum: K = 1,
children: re
}) => {
const j = M(null), I = M(null), v = M(null), P = M(null), [W, N] = E([]), [g, w] = E(0), [f, m] = E(1), se = te(g), ie = te(f), [k, G] = E(0), [L, R] = E(!1), [a, A] = E(
null
), [z, le] = E(!0), e = M({
$_itemsIntersectionObserver: null,
$_guardIntersectionObserver: null,
$_loadingIO: null,
$_scrollEl: null,
$_maxViewNum: 0,
$_lastDatalength: 0,
$_destroyed: !1,
$_refreshTimer: null,
$_scrollTimer: null,
$_loadingTimer: null,
$_iOOptions: void 0,
$_startGuardEntry: null,
$_endGuardEntry: null,
$_loadingEl: null,
$_startIndex: 0,
$_endIndex: 1,
$_lastStartIndex: 0,
$_lastEndIndex: 1,
$_data: [],
$_domHeightCache: null,
$_loading: !1
}).current, y = B(() => !!i && i > 0, [i]), b = B(
() => i || d || 0,
[i, d]
), q = B(() => {
if (y)
return g * b;
if (!a) return 0;
const r = l.slice(0, g).map((c) => c[p]);
return a.getHeightByIds(r);
}, [g, a]), oe = B(() => {
if (y) {
const o = (f - g) * b;
return k - q - o;
}
if (!a) return 0;
const r = l.slice(g, f).map((o) => o[p]), n = a.getHeightByIds(r);
return k - q - n;
}, [g, f, k, a]), ce = () => {
if (le(!1), e.$_destroyed = !1, e.$_lastDatalength = l.length, ee(l), t) {
N(l);
return;
}
if (e.$_scrollEl = j.current?.parentNode, e.$_scrollEl && (e.$_scrollEl.style.overflowAnchor = "none", pe(e.$_scrollEl)), e.$_iOOptions = {
root: e.$_scrollEl,
rootMargin: `${x[0]}px 0px ${x[1]}px 0px`
}, y)
Q(), G(l.length * b);
else {
const n = new xe(p);
n.fill(l, b), A(n), e.$_domHeightCache = n, Q();
}
de();
const r = J();
e.$_maxViewNum = Math.max(
Math.ceil(r / b),
K
), m(e.$_maxViewNum), e.$_scrollEl?.addEventListener("scroll", U);
};
O(() => {
z || (e.$_guardIntersectionObserver && (I.current && e.$_guardIntersectionObserver.observe(I.current), v.current && e.$_guardIntersectionObserver.observe(v.current)), ue(), he());
}, [z]);
const ae = () => {
e.$_scrollEl?.removeEventListener("scroll", U), e.$_loadingEl && e.$_scrollEl?.parentNode && e.$_scrollEl.parentNode.removeChild(e.$_loadingEl), e.$_scrollEl = null, N([]), w(0), m(e.$_maxViewNum || 1), e.$_itemsIntersectionObserver?.disconnect(), e.$_guardIntersectionObserver?.disconnect(), e.$_itemsIntersectionObserver = null, e.$_guardIntersectionObserver = null, e.$_refreshTimer && clearTimeout(e.$_refreshTimer), e.$_scrollTimer && clearTimeout(e.$_scrollTimer), e.$_refreshTimer = null, e.$_scrollTimer = null, a?.empty(), A(null), G(0), e.$_loadingIO?.disconnect(), e.$_loadingIO = null;
}, de = () => {
if (!e.$_scrollEl) return;
const r = document.createElement("div");
r.className = "async-virtual-scroll-loading", r.innerHTML = `<div class="async-virtual-scroll-loading-icon">${Ie}</div>`, e.$_scrollEl.insertAdjacentElement("afterend", r), e.$_loadingEl = r;
}, J = () => {
if (!e.$_scrollEl) return 0;
const r = x[0] || 0, n = x[1] || 0;
let c = e.$_scrollEl.offsetHeight + r + n;
const o = window.getComputedStyle(e.$_scrollEl), s = parseFloat(o.maxHeight);
return !isNaN(s) && s > 0 && (c = s + r + n), c;
}, ue = () => {
e.$_itemsIntersectionObserver = new IntersectionObserver(
(r) => {
for (const n of r)
if (n.isIntersecting) {
const c = n.target, o = c.getAttribute(ne);
a?.get(o)?.height !== c.offsetHeight && (a?.set(o, c.offsetHeight), A(F(a)));
}
},
e.$_iOOptions
);
}, Q = () => {
e.$_guardIntersectionObserver = new IntersectionObserver(
(r) => {
const n = e.$_startIndex, c = e.$_endIndex, o = e.$_data;
for (const s of r)
if (s.target === I.current)
e.$_startGuardEntry = s, s.isIntersecting && (c - n > e.$_maxViewNum && m(
Math.min(n + e.$_maxViewNum, o.length)
), w((h) => Math.max(0, h - e.$_maxViewNum)));
else if (s.target === v.current && (e.$_endGuardEntry = s, s.isIntersecting)) {
if (c === o.length) return;
c - n > e.$_maxViewNum && w(() => Math.max(c - e.$_maxViewNum, 0)), m((h) => Math.min(h + e.$_maxViewNum, o.length));
}
},
e.$_iOOptions
);
}, he = () => {
e.$_loadingIO = new IntersectionObserver(
(r) => {
const n = e.$_startIndex, c = e.$_endIndex, o = e.$_data;
for (const s of r)
s.target === I.current && n !== 0 && s.isIntersecting && R(!0), s.target === v.current && c !== o.length && s.isIntersecting && R(!0);
},
{ root: e.$_scrollEl }
), I.current && e.$_loadingIO.observe(I.current), v.current && e.$_loadingIO.observe(v.current);
}, ge = () => {
e.$_startIndex = g, e.$_endIndex = f, e.$_lastStartIndex = se, e.$_lastEndIndex = ie, N(l.slice(g, f));
};
O(() => {
if (y) return;
const r = Array.from(P.current?.children || []);
for (const n of r)
e.$_itemsIntersectionObserver?.observe(n);
}, [W]);
const $e = () => {
(e.$_startGuardEntry?.isIntersecting || e.$_endGuardEntry?.isIntersecting) && _e(), e.$_loading && R(!1);
}, U = () => {
window.requestAnimationFrame(() => {
e.$_scrollTimer && clearTimeout(e.$_scrollTimer), e.$_loading || (e.$_loadingTimer && clearTimeout(e.$_loadingTimer), e.$_loadingTimer = setTimeout(() => {
e.$_loadingTimer = null, I.current && e.$_loadingIO?.observe(I.current), v.current && e.$_loadingIO?.observe(v.current);
}, 150)), e.$_scrollTimer = setTimeout(() => {
requestIdleCallback($e, {
timeout: 500
});
}, 500);
});
}, _e = () => {
const r = e.$_startIndex, n = e.$_endIndex, c = e.$_lastStartIndex, o = e.$_lastEndIndex, s = e.$_data;
if (c === r && o === n || n === s.length || !e.$_scrollEl) return;
const $ = e.$_scrollEl, h = $.scrollTop, C = $.clientHeight;
if (y && i) {
const _ = Math.max(0, Math.floor((h - x[0]) / i));
let H = Math.min(
s.length,
Math.ceil((h + C + x[1]) / i)
);
H === _ && (H = s.length), w(_), m(H);
return;
}
if (!e.$_domHeightCache) return;
const X = e.$_domHeightCache;
let Y = 0, T = 0;
for (let _ = 0; _ < s.length; _++) {
const H = s[_][p], V = X.get(H)?.height || 0;
if (Y + V >= h - x[0]) {
T = _;
break;
}
Y += V;
}
let S = T, Z = 0;
for (let _ = T; _ < s.length; _++) {
const H = s[_][p], V = X.get(H)?.height || 0;
if (Z += V, Z >= C + x[1] + x[0]) {
S = _ + 1;
break;
}
}
S === T && (S = s.length), w(T), m(S);
};
return O(() => {
u ? ce() : (e.$_destroyed = !0, ae());
}, [u]), O(() => {
e.$_destroyed || t || ge();
}, [g, f]), O(() => {
if (!a?.updateAt || i) return;
G(a.total() || 0);
const n = a.getAll().filter((o) => o.height !== d), c = n.length;
if (c) {
const o = n.map((h) => h.height).reduce((h, C) => h + C, 0), s = Math.ceil(o / c), $ = J();
e.$_maxViewNum = Math.max(
Math.ceil($ / s),
K
);
}
}, [a?.updateAt]), O(() => {
if (e.$_destroyed) return;
if (e.$_data = l, ee(l), t) {
N(l);
return;
}
y || e.$_itemsIntersectionObserver?.disconnect();
const r = f, n = l.length, c = r === e.$_lastDatalength;
if (n > e.$_lastDatalength)
f < e.$_maxViewNum ? m(Math.min(e.$_maxViewNum, n)) : c && m(n);
else if (n < e.$_lastDatalength) {
if (g >= n)
w(0), m(Math.min(e.$_maxViewNum, n));
else if (f > l.length) {
m(l.length);
const o = f - g;
o < e.$_maxViewNum && w(Math.max(0, g - o));
}
}
e.$_lastDatalength = n, !y && a && (e.$_refreshTimer && clearTimeout(e.$_refreshTimer), e.$_refreshTimer = setTimeout(() => {
for (const $ of l) {
const h = String($[p]);
a.has(h) || a.set(h, d || 0);
}
const o = l.map(($) => String($[p])), s = [];
a.forEach(($) => {
o.includes($.key) || s.push($.key);
}), a.clearByKeys(s), e.$_refreshTimer = null, A(F(a));
}, 100));
}, [l]), O(() => {
e.$_loading = L, e.$_loadingEl && (e.$_loadingEl.style.display = L ? "flex" : "none");
}, [L]), /* @__PURE__ */ fe(
"div",
{
className: "async-virtual-scroll",
ref: j,
style: { minHeight: `${k}px` },
children: [
/* @__PURE__ */ D(
"div",
{
ref: I,
className: "start",
style: { height: `${q}px` }
}
),
/* @__PURE__ */ D("div", { ref: P, children: W.map((r) => /* @__PURE__ */ D(me.Fragment, { children: re(r, r.$_index, r[p]) }, r[p])) }),
/* @__PURE__ */ D(
"div",
{
ref: v,
className: "end",
style: { height: `${oe}px` }
}
)
]
}
);
};
export {
ye as default
};