perf: virtual list

pull/5362/head
tangjinzhou 2022-03-18 16:55:15 +08:00
parent 9bd52d4ca0
commit bc5928ec42
4 changed files with 52 additions and 31 deletions

View File

@ -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'],

View File

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

View File

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

View File

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