perf: virtual list
parent
9bd52d4ca0
commit
bc5928ec42
|
@ -61,7 +61,7 @@ const Select = defineComponent({
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
props: initDefaultProps(selectProps(), {
|
props: initDefaultProps(selectProps(), {
|
||||||
listHeight: 256,
|
listHeight: 256,
|
||||||
listItemHeight: 24,
|
listItemHeight: 32,
|
||||||
}),
|
}),
|
||||||
SECRET_COMBOBOX_MODE_DO_NOT_USE,
|
SECRET_COMBOBOX_MODE_DO_NOT_USE,
|
||||||
// emits: ['change', 'update:value', 'blur'],
|
// emits: ['change', 'update:value', 'blur'],
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import type { PropType, Component, CSSProperties } from 'vue';
|
import type { PropType, Component, CSSProperties } from 'vue';
|
||||||
import {
|
import {
|
||||||
|
shallowRef,
|
||||||
|
toRaw,
|
||||||
onMounted,
|
onMounted,
|
||||||
onUpdated,
|
onUpdated,
|
||||||
ref,
|
ref,
|
||||||
|
@ -113,20 +115,36 @@ const List = defineComponent({
|
||||||
scrollTop: 0,
|
scrollTop: 0,
|
||||||
scrollMoving: false,
|
scrollMoving: false,
|
||||||
});
|
});
|
||||||
|
const data = computed(() => {
|
||||||
const mergedData = computed(() => {
|
|
||||||
return props.data || EMPTY_DATA;
|
return props.data || EMPTY_DATA;
|
||||||
});
|
});
|
||||||
|
const mergedData = shallowRef([]);
|
||||||
|
watch(
|
||||||
|
data,
|
||||||
|
() => {
|
||||||
|
mergedData.value = toRaw(data.value).slice();
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const itemKey = shallowRef((_item: Record<string, any>) => undefined);
|
||||||
|
watch(
|
||||||
|
() => props.itemKey,
|
||||||
|
val => {
|
||||||
|
if (typeof val === 'function') {
|
||||||
|
itemKey.value = val;
|
||||||
|
} else {
|
||||||
|
itemKey.value = item => item?.[val];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
const componentRef = ref<HTMLDivElement>();
|
const componentRef = ref<HTMLDivElement>();
|
||||||
const fillerInnerRef = ref<HTMLDivElement>();
|
const fillerInnerRef = ref<HTMLDivElement>();
|
||||||
const scrollBarRef = ref<any>(); // Hack on scrollbar to enable flash call
|
const scrollBarRef = ref<any>(); // Hack on scrollbar to enable flash call
|
||||||
// =============================== Item Key ===============================
|
// =============================== Item Key ===============================
|
||||||
const getKey = (item: Record<string, any>) => {
|
const getKey = (item: Record<string, any>) => {
|
||||||
if (typeof props.itemKey === 'function') {
|
return itemKey.value(item);
|
||||||
return props.itemKey(item);
|
|
||||||
}
|
|
||||||
return item?.[props.itemKey];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const sharedConfig = {
|
const sharedConfig = {
|
||||||
|
@ -217,7 +235,6 @@ const List = defineComponent({
|
||||||
() => state.scrollTop,
|
() => state.scrollTop,
|
||||||
mergedData,
|
mergedData,
|
||||||
updatedMark,
|
updatedMark,
|
||||||
heights,
|
|
||||||
() => props.height,
|
() => props.height,
|
||||||
offsetHeight,
|
offsetHeight,
|
||||||
],
|
],
|
||||||
|
@ -232,21 +249,27 @@ const List = defineComponent({
|
||||||
let endIndex: number | undefined;
|
let endIndex: number | undefined;
|
||||||
const dataLen = mergedData.value.length;
|
const dataLen = mergedData.value.length;
|
||||||
const data = mergedData.value;
|
const data = mergedData.value;
|
||||||
|
const scrollTop = state.scrollTop;
|
||||||
|
const { itemHeight, height } = props;
|
||||||
|
const scrollTopHeight = scrollTop + height;
|
||||||
|
|
||||||
for (let i = 0; i < dataLen; i += 1) {
|
for (let i = 0; i < dataLen; i += 1) {
|
||||||
const item = data[i];
|
const item = data[i];
|
||||||
const key = getKey(item);
|
const key = getKey(item);
|
||||||
|
|
||||||
const cacheHeight = heights.value[key];
|
let cacheHeight = heights.get(key);
|
||||||
const currentItemBottom =
|
if (cacheHeight === undefined) {
|
||||||
itemTop + (cacheHeight === undefined ? props.itemHeight! : cacheHeight);
|
cacheHeight = itemHeight;
|
||||||
|
}
|
||||||
|
const currentItemBottom = itemTop + cacheHeight;
|
||||||
|
|
||||||
if (currentItemBottom >= state.scrollTop && startIndex === undefined) {
|
if (startIndex === undefined && currentItemBottom >= scrollTop) {
|
||||||
startIndex = i;
|
startIndex = i;
|
||||||
startOffset = itemTop;
|
startOffset = itemTop;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check item bottom in the range. We will render additional one item for motion usage
|
// Check item bottom in the range. We will render additional one item for motion usage
|
||||||
if (currentItemBottom > state.scrollTop + props.height! && endIndex === undefined) {
|
if (endIndex === undefined && currentItemBottom > scrollTopHeight) {
|
||||||
endIndex = i;
|
endIndex = i;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
import type { VNodeProps, ComputedRef, Ref } from 'vue';
|
import type { VNodeProps, Ref, ShallowRef } from 'vue';
|
||||||
import { shallowRef, watch, ref } from 'vue';
|
import { watch, ref } from 'vue';
|
||||||
import type { GetKey } from '../interface';
|
import type { GetKey } from '../interface';
|
||||||
|
|
||||||
type CacheMap = Ref<Record<string, number>>;
|
export type CacheMap = Map<any, number>;
|
||||||
|
|
||||||
export default function useHeights<T>(
|
export default function useHeights<T>(
|
||||||
mergedData: ComputedRef<any[]>,
|
mergedData: ShallowRef<any[]>,
|
||||||
getKey: GetKey<T>,
|
getKey: GetKey<T>,
|
||||||
onItemAdd?: ((item: T) => void) | null,
|
onItemAdd?: ((item: T) => void) | null,
|
||||||
onItemRemove?: ((item: T) => void) | null,
|
onItemRemove?: ((item: T) => void) | null,
|
||||||
): [(item: T, instance: HTMLElement) => void, () => void, CacheMap, Ref<Symbol>] {
|
): [(item: T, instance: HTMLElement) => void, () => void, CacheMap, Ref<Symbol>] {
|
||||||
const instance = new Map<VNodeProps['key'], HTMLElement>();
|
const instance = new Map<VNodeProps['key'], HTMLElement>();
|
||||||
const heights = shallowRef({});
|
let heights = new Map();
|
||||||
const updatedMark = ref(Symbol('update'));
|
const updatedMark = ref(Symbol('update'));
|
||||||
watch(mergedData, () => {
|
watch(mergedData, () => {
|
||||||
heights.value = {};
|
heights = new Map();
|
||||||
updatedMark.value = Symbol('update');
|
updatedMark.value = Symbol('update');
|
||||||
});
|
});
|
||||||
let heightUpdateId = 0;
|
let heightUpdateId = 0;
|
||||||
|
@ -28,10 +28,10 @@ export default function useHeights<T>(
|
||||||
instance.forEach((element, key) => {
|
instance.forEach((element, key) => {
|
||||||
if (element && element.offsetParent) {
|
if (element && element.offsetParent) {
|
||||||
const { offsetHeight } = element;
|
const { offsetHeight } = element;
|
||||||
if (heights.value[key!] !== offsetHeight) {
|
if (heights.get(key) !== offsetHeight) {
|
||||||
//changed = true;
|
//changed = true;
|
||||||
updatedMark.value = Symbol('update');
|
updatedMark.value = Symbol('update');
|
||||||
heights.value[key!] = element.offsetHeight;
|
heights.set(key, element.offsetHeight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import type { Data } from '../../_util/type';
|
import type { ShallowRef, Ref } from 'vue';
|
||||||
import type { ComputedRef, Ref } from 'vue';
|
|
||||||
import raf from '../../_util/raf';
|
import raf from '../../_util/raf';
|
||||||
import type { GetKey } from '../interface';
|
import type { GetKey } from '../interface';
|
||||||
|
import type { CacheMap } from './useHeights';
|
||||||
|
|
||||||
export default function useScrollTo(
|
export default function useScrollTo(
|
||||||
containerRef: Ref<Element | undefined>,
|
containerRef: Ref<Element | undefined>,
|
||||||
mergedData: ComputedRef<any[]>,
|
mergedData: ShallowRef<any[]>,
|
||||||
heights: Ref<Data>,
|
heights: CacheMap,
|
||||||
props,
|
props,
|
||||||
getKey: GetKey,
|
getKey: GetKey,
|
||||||
collectHeight: () => void,
|
collectHeight: () => void,
|
||||||
|
@ -58,11 +58,10 @@ export default function useScrollTo(
|
||||||
let itemBottom = 0;
|
let itemBottom = 0;
|
||||||
|
|
||||||
const maxLen = Math.min(data.length, index);
|
const maxLen = Math.min(data.length, index);
|
||||||
|
|
||||||
for (let i = 0; i <= maxLen; i += 1) {
|
for (let i = 0; i <= maxLen; i += 1) {
|
||||||
const key = getKey(data[i]);
|
const key = getKey(data[i]);
|
||||||
itemTop = stackTop;
|
itemTop = stackTop;
|
||||||
const cacheHeight = heights.value[key!];
|
const cacheHeight = heights.get(key);
|
||||||
itemBottom = itemTop + (cacheHeight === undefined ? itemHeight : cacheHeight);
|
itemBottom = itemTop + (cacheHeight === undefined ? itemHeight : cacheHeight);
|
||||||
|
|
||||||
stackTop = itemBottom;
|
stackTop = itemBottom;
|
||||||
|
@ -71,7 +70,7 @@ export default function useScrollTo(
|
||||||
needCollectHeight = true;
|
needCollectHeight = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const scrollTop = containerRef.value.scrollTop;
|
||||||
// Scroll to
|
// Scroll to
|
||||||
let targetTop: number | null = null;
|
let targetTop: number | null = null;
|
||||||
|
|
||||||
|
@ -84,7 +83,6 @@ export default function useScrollTo(
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
const { scrollTop } = containerRef.value;
|
|
||||||
const scrollBottom = scrollTop + height;
|
const scrollBottom = scrollTop + height;
|
||||||
if (itemTop < scrollTop) {
|
if (itemTop < scrollTop) {
|
||||||
newTargetAlign = 'top';
|
newTargetAlign = 'top';
|
||||||
|
@ -94,7 +92,7 @@ export default function useScrollTo(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetTop !== null && targetTop !== containerRef.value.scrollTop) {
|
if (targetTop !== null && targetTop !== scrollTop) {
|
||||||
syncScrollTop(targetTop);
|
syncScrollTop(targetTop);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue