1616 left: 0,
1717 right: 0,
1818 transform: `translateY(${offsetY}px)`,
19- willChange: 'transform',
2019 }"
2120 >
2221 <div
3635
3736<script setup lang="ts">
3837import { NScrollbar } from " naive-ui" ;
39- import { useElementSize } from " @vueuse/core" ;
4038
4139interface Props {
4240 /** 列表项数据 */
@@ -118,15 +116,21 @@ const initializeHeights = () => {
118116};
119117
120118// 更新累积高度
121- const updateTops = () => {
119+ const updateTops = (fromIndex = 0 ) => {
122120 if (props .itemFixed ) return ;
123121
124- itemTops .value = [];
125- let top = 0 ;
126- for (let i = 0 ; i < itemHeights .value .length ; i ++ ) {
127- itemTops .value [i ] = top ;
128- top += itemHeights .value [i ];
122+ const heights = itemHeights .value ;
123+ const tops =
124+ itemTops .value .length === heights .length ? itemTops .value : new Array (heights .length );
125+
126+ // 从变更位置开始计算
127+ let top = fromIndex > 0 ? tops [fromIndex - 1 ] + heights [fromIndex - 1 ] : 0 ;
128+ for (let i = fromIndex ; i < heights .length ; i ++ ) {
129+ tops [i ] = top ;
130+ top += heights [i ];
129131 }
132+
133+ itemTops .value = tops ;
130134};
131135
132136// 列表总高度
@@ -159,33 +163,38 @@ const calculateVisibleRange = (currentScrollTop: number) => {
159163 const visibleCount = Math .ceil (vHeight / props .itemHeight );
160164 endIndex = startIndex + visibleCount ;
161165 } else {
162- // 动态高度模式
163- let start = 0 ;
164- let end = itemTops .value .length - 1 ;
165-
166- while (start <= end ) {
167- const mid = Math .floor ((start + end ) / 2 );
168- const top = itemTops .value [mid ];
169- const bottom = top + itemHeights .value [mid ];
170-
166+ // 动态高度模式 - 使用二分查找优化
167+ const tops = itemTops .value ;
168+ const heights = itemHeights .value ;
169+ const len = tops .length ;
170+
171+ // 二分查找起始索引
172+ let lo = 0 ;
173+ let hi = len - 1 ;
174+ while (lo <= hi ) {
175+ const mid = (lo + hi ) >>> 1 ;
176+ const bottom = tops [mid ] + heights [mid ];
171177 if (bottom > currentScrollTop ) {
172178 startIndex = mid ;
173- end = mid - 1 ;
179+ hi = mid - 1 ;
174180 } else {
175- start = mid + 1 ;
181+ lo = mid + 1 ;
176182 }
177183 }
178184
179- // 查找结束索引
185+ // 二分查找结束索引
180186 const viewportBottom = currentScrollTop + vHeight ;
187+ lo = startIndex ;
188+ hi = len - 1 ;
181189 endIndex = startIndex ;
182-
183- // 从 startIndex 开始向后查找
184- for (let i = startIndex ; i < itemTops .value .length ; i ++ ) {
185- if (itemTops .value [i ] > viewportBottom ) {
186- break ;
190+ while (lo <= hi ) {
191+ const mid = (lo + hi ) >>> 1 ;
192+ if (tops [mid ] <= viewportBottom ) {
193+ endIndex = mid ;
194+ lo = mid + 1 ;
195+ } else {
196+ hi = mid - 1 ;
187197 }
188- endIndex = i ;
189198 }
190199 }
191200
@@ -212,7 +221,7 @@ const offsetY = computed(() => {
212221 return actualStartIndex .value * props .itemHeight ;
213222 }
214223 if (itemTops .value .length === 0 ) return 0 ;
215- return itemTops .value [actualStartIndex .value ];
224+ return Math . round ( itemTops .value [actualStartIndex .value ]) ;
216225});
217226
218227// 测量项目高度
@@ -246,28 +255,36 @@ const measureItemHeights = () => {
246255 }
247256};
248257
258+ let rafId: number | null = null ;
259+ let pendingScrollTarget: HTMLElement | null = null ;
260+
261+ const processScroll = () => {
262+ rafId = null ;
263+ const target = pendingScrollTarget ;
264+ if (! target ) return ;
265+
266+ const { scrollTop : st, scrollHeight, clientHeight } = target ;
267+ scrollTop .value = st ;
268+ calculateVisibleRange (st );
269+
270+ // 触底检测
271+ if (scrollHeight - st - clientHeight < 50 ) {
272+ emit (" reachBottom" );
273+ }
274+ };
275+
249276// 处理滚动事件
250- let ticking = false ;
251277const handleScroll = (event : Event ) => {
252278 const target = event .target as HTMLElement ;
253279 if (! target ) return ;
254280
255281 // 触发外部事件
256282 emit (" scroll" , event );
257283
258- if (! ticking ) {
259- requestAnimationFrame (() => {
260- const { scrollTop : st, scrollHeight, clientHeight } = target ;
261- scrollTop .value = st ;
262- calculateVisibleRange (st );
263-
264- // 触底检测
265- if (scrollHeight - st - clientHeight < 50 ) {
266- emit (" reachBottom" );
267- }
268- ticking = false ;
269- });
270- ticking = true ;
284+ // 合并多次滚动到一个 rAF
285+ pendingScrollTarget = target ;
286+ if (rafId === null ) {
287+ rafId = requestAnimationFrame (processScroll );
271288 }
272289};
273290
@@ -312,14 +329,17 @@ defineExpose({
312329 getScrollTop ,
313330});
314331
332+ // 防抖高度测量
333+ const debouncedMeasure = useDebounceFn (measureItemHeights , 50 );
334+
315335// 监听数据变化
316336watch (
317337 () => props .items ,
318338 () => {
319339 initializeHeights ();
320340 calculateVisibleRange (scrollTop .value );
321341 // 重新测量高度
322- nextTick (measureItemHeights );
342+ nextTick (debouncedMeasure );
323343 },
324344 { deep: false },
325345);
@@ -343,13 +363,12 @@ watch(
343363 () => [actualStartIndex .value , actualEndIndex .value ],
344364 () => {
345365 if (! props .itemFixed ) {
346- nextTick (measureItemHeights );
366+ nextTick (debouncedMeasure );
347367 }
348368 },
349369 { flush: " post" },
350370);
351371
352- // 组件挂载后初始化
353372onMounted (() => {
354373 initializeHeights ();
355374 // 等待 DOM 渲染和容器尺寸确定
@@ -362,6 +381,13 @@ onMounted(() => {
362381 if (! props .itemFixed ) measureItemHeights ();
363382 });
364383});
384+
385+ onUnmounted (() => {
386+ if (rafId !== null ) {
387+ cancelAnimationFrame (rafId );
388+ rafId = null ;
389+ }
390+ });
365391 </script >
366392
367393<style scoped>
0 commit comments