UNPKG

@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
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 };