@jason12306/vue-async-virtual-scroll
Version:
High-performance asynchronous virtual scroll component for Vue 3 with dynamic height support and optimized rendering for large datasets
355 lines (354 loc) • 13 kB
JavaScript
import { defineComponent as ge, mergeModels as me, shallowRef as O, ref as I, computed as N, useModel as pe, watch as B, createElementBlock as ee, openBlock as te, normalizeStyle as L, createElementVNode as G, Fragment as ye, renderList as be, renderSlot as ke, mergeProps as Ie, nextTick as le } from "vue";
class Se {
uniqueKey;
bucket;
updateAt;
constructor(e = "id") {
this.uniqueKey = e, this.bucket = /* @__PURE__ */ new Map(), this.updateAt = 0;
}
/**
* 用指定高度填充所有数据项
*/
fill(e, u) {
this.bucket.clear();
for (const c of e) {
const r = String(c[this.uniqueKey]);
this.bucket.set(r, {
height: u,
key: r
});
}
this.updateAt = Date.now();
}
/**
* 获取指定 key 的缓存高度对象
*/
get(e) {
return this.bucket.get(String(e));
}
/**
* 获取所有缓存的高度对象
*/
getAll() {
return [...this.bucket.values()];
}
/**
* 设置指定 key 的高度
*/
set(e, u) {
const c = String(e);
if (this.bucket.has(c)) {
const r = this.bucket.get(c);
this.bucket.set(c, {
...r,
height: u
});
} else
this.bucket.set(c, {
key: c,
height: u
});
this.updateAt = Date.now();
}
/**
* 获取所有缓存高度的总和
*/
total() {
return this.getAll().map((e) => e.height).reduce((e, u) => e + u, 0);
}
/**
* 根据一组 id 获取这些项的高度总和
*/
getHeightByIds(e) {
const u = [];
for (const c of e)
u.push(this.get(String(c))?.height || 0);
return u.reduce((c, r) => c + r, 0);
}
/**
* 判断是否存在指定 key 的缓存
*/
has(e) {
return this.bucket.has(e);
}
/**
* 遍历所有缓存项
*/
forEach(e) {
this.getAll().forEach((u) => {
e(u);
});
}
/**
* 清除指定 key 的缓存
*/
clearByKeys(e) {
for (const u of e)
this.bucket.delete(String(u));
this.updateAt = Date.now();
}
/**
* 清空所有缓存
*/
empty() {
this.bucket.clear();
}
}
function ae(h) {
let e = 0;
for (const u of h)
u.$_index = e, e++;
}
function xe(h) {
if (h.parentNode && h.parentNode.classList.contains("async-virtual-scroll-wrapper"))
return;
const e = document.createElement("div");
e.className = "async-virtual-scroll-wrapper", e.style.position = "relative", h.parentNode.insertBefore(e, h), e.appendChild(h);
}
const we = '<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>', He = /* @__PURE__ */ ge({
name: "AsyncVirtualScroll",
__name: "AsyncVirtualScroll",
props: /* @__PURE__ */ me({
value: { type: Boolean, default: !1 },
ban: { type: Boolean, default: !1 },
itemSize: { default: void 0 },
minItemSize: { default: void 0 },
data: { default: () => [] },
buffer: { default: () => [200, 200] },
keyField: { default: "id" },
dataUid: { default: "data-uid" },
viewNum: { default: 1 }
}, {
modelValue: { required: !0 },
modelModifiers: {}
}),
emits: ["update:modelValue"],
setup(h) {
const e = h;
let u = null, c = null, r = null, v = 0, _ = 0, C = !0, z, $, F, R = null, V = 0, q = 0, K = !0, U = null, W = null, E = null;
const j = O(), S = O(), x = O(), P = O(), A = I([]), i = I(0), a = I(0), s = I(null), w = I(0), M = I(!1), y = I(null), k = N(() => e.itemSize && e.itemSize > 0), T = N(() => e.itemSize || e.minItemSize || 0), D = N(() => {
if (k.value)
return i.value * T.value;
if (!s.value) return 0;
const l = e.data.slice(0, i.value).map((t) => t[e.keyField]);
return s.value.getHeightByIds(l);
}), ne = N(() => {
if (k.value) {
const n = (a.value - i.value) * T.value;
return w.value - D.value - n;
}
if (!s.value) return 0;
const l = e.data.slice(i.value, a.value).map((n) => n[e.keyField]), t = s.value.getHeightByIds(l);
return w.value - D.value - t;
}), ie = N(() => [i.value, a.value]);
function re() {
if (C = !1, _ = e.data.length, ae(e.data), e.ban) {
A.value = e.data;
return;
}
r = j.value?.parentNode, r && (r.style.overflowAnchor = "none", xe(r)), R = {
root: r,
rootMargin: `${e.buffer[0]}px 0px ${e.buffer[1]}px 0px`
}, k.value ? (J(), c.observe(S.value), c.observe(x.value), w.value = e.data.length * T.value) : (s.value = new Se(e.keyField), s.value.fill(e.data, T.value), oe(), J()), fe();
const l = Y();
v = Math.max(Math.ceil(l / T.value), e.viewNum), a.value = v, r?.addEventListener("scroll", X), ce();
}
function ue() {
i.value = Math.max(0, i.value - v);
}
function se() {
a.value = Math.min(a.value + v, e.data.length);
}
function oe() {
u = new IntersectionObserver(
(l) => {
for (const t of l)
if (t.isIntersecting) {
const n = t.target, o = n.getAttribute(e.dataUid);
s.value?.get(o)?.height !== n.offsetHeight && s.value?.set(o, n.offsetHeight);
}
},
{
...R
}
);
}
function J() {
c = new IntersectionObserver(
(l) => {
for (const t of l)
if (t.target === S.value)
U = t, t.isIntersecting && (a.value - i.value > v && (a.value = Math.min(i.value + v, e.data.length)), ue());
else if (t.target === x.value && (W = t, t.isIntersecting)) {
if (a.value === e.data.length)
return;
a.value - i.value > v && (i.value = Math.max(a.value - v, 0)), se();
}
},
{
...R
}
);
}
function ce() {
E = new IntersectionObserver(
(l) => {
for (const t of l)
t.target === S.value && i.value !== 0 && t.isIntersecting && (M.value = !0), t.target === x.value && a.value !== e.data.length && t.isIntersecting && (M.value = !0);
},
{
root: r
}
);
}
function Q() {
A.value = e.data.slice(i.value, a.value), !k.value && le(() => {
const l = Array.from(P.value?.children || []);
for (const t of l)
u?.observe(t);
});
}
function de() {
if (V === i.value && q === a.value)
return;
const l = r, t = l.scrollTop, n = l.clientHeight, o = e.buffer;
if (k.value) {
const d = e.itemSize, H = Math.max(0, Math.floor((t - o[0]) / d));
let b = Math.min(
e.data.length,
Math.ceil((t + n + o[1]) / d)
);
b === H && (b = e.data.length), i.value = H, a.value = b;
return;
}
let g = 0, f = 0;
for (let d = 0; d < e.data.length; d++) {
const H = e.data[d][e.keyField], b = s.value?.get(H)?.height || 0;
if (g + b >= t - o[0]) {
f = d;
break;
}
g += b;
}
let m = f, p = 0;
for (let d = f; d < e.data.length; d++) {
const H = e.data[d][e.keyField], b = s.value?.get(H)?.height || 0;
if (p += b, p >= n + o[1] + o[0]) {
m = d + 1;
break;
}
}
m === f && (m = e.data.length), i.value = f, a.value = m;
}
async function ve() {
(U?.isIntersecting || W?.isIntersecting) && de(), M.value && le(() => {
M.value = !1;
});
}
function X(l) {
window.requestAnimationFrame(() => {
clearTimeout($), M.value || (clearTimeout(F), F = setTimeout(() => {
clearTimeout(F), F = void 0, E?.observe(S.value), E?.observe(x.value);
}, 150)), $ = setTimeout(() => {
let t = 0;
cancelIdleCallback(t), t = requestIdleCallback(
() => {
ve();
},
{
timeout: 500
}
), $ = void 0;
}, 500);
});
}
function fe() {
y.value = document.createElement("div"), y.value.className = "async-virtual-scroll-loading", y.value.innerHTML = `<div class="async-virtual-scroll-loading-icon">${we}</div>`, r?.insertAdjacentElement("afterend", y.value);
}
function Y() {
const l = e.buffer?.[0] || 0, t = e.buffer?.[1] || 0;
let n = r.offsetHeight + l + t;
const o = window.getComputedStyle(r), g = parseFloat(o.maxHeight);
return !isNaN(g) && g > 0 && (n = g + l + t), n;
}
function he() {
r?.removeEventListener("scroll", X), r?.parentNode && y.value && r.parentNode.removeChild(y.value), r = void 0, A.value = [], i.value = 0, a.value = v || 1, u?.disconnect(), c?.disconnect(), u = null, c = null, clearTimeout(z), clearTimeout($), z = void 0, $ = void 0, s.value?.empty(), s.value = null, w.value = 0, E?.disconnect(), E = null;
}
const Z = pe(h, "modelValue");
return B(Z, (l) => {
l ? re() : (C = !0, he());
}), B(ie, (l, t) => {
C || e.ban || (V = t[0], q = t[1], Q());
}), B(
() => s.value?.updateAt,
() => {
if (e.itemSize || !s.value)
return;
w.value = s.value.total() || 0;
const t = s.value.getAll().filter((o) => o.height !== e.minItemSize), n = t.length;
if (n) {
const o = t.map((m) => m.height).reduce((m, p) => m + p, 0), g = Math.ceil(o / n), f = Y();
v = Math.max(Math.ceil(f / g), e.viewNum), K && (K = !1, c.observe(S.value), c.observe(x.value));
}
}
), B(e.data, (l) => {
if (C || !Z.value) return;
if (ae(e.data), e.ban) {
A.value = e.data;
return;
}
k.value || u?.disconnect();
const t = i.value, n = a.value, o = l.length, g = n === _;
if (o > _)
a.value < v ? a.value = Math.min(v, o) : g && (a.value = o);
else if (o < _) {
if (i.value >= l.length)
i.value = 0, a.value = Math.min(v, o);
else if (a.value > e.data.length) {
a.value = e.data.length;
const f = a.value - i.value;
f < v && (i.value = Math.max(0, i.value - f));
}
}
t === i.value && n === a.value && Q(), _ = l.length, k.value || (clearTimeout(z), z = setTimeout(() => {
for (const p of e.data) {
const d = String(p[e.keyField]);
s.value.has(d) || s.value.set(d, e.minItemSize);
}
const f = e.data.map((p) => String(p[e.keyField])), m = [];
s.value.forEach((p) => {
f.includes(p.key) || m.push(p.key);
}), s.value.clearByKeys(m), z = void 0;
}, 100));
}), B(M, (l) => {
y.value && (y.value.style.display = l ? "flex" : "none");
}), (l, t) => (te(), ee("div", {
class: "async-virtual-scroll",
ref_key: "optimizeScrollRef",
ref: j,
style: L({ minHeight: w.value + "px" })
}, [
G("div", {
ref_key: "startGuardRef",
ref: S,
class: "start",
style: L({ height: `${D.value}px` })
}, null, 4),
G("div", {
ref_key: "slotWrapperRef",
ref: P
}, [
(te(!0), ee(ye, null, be(A.value, (n) => ke(l.$slots, "default", Ie({ ref_for: !0 }, { item: n, index: n.$_index, dataUid: n[l.keyField] }))), 256))
], 512),
G("div", {
ref_key: "endGuardRef",
ref: x,
style: L({ height: `${ne.value}px` }),
class: "end"
}, null, 4)
], 4));
}
});
export {
He as default
};