From 6b1ebb8cd5a9d6320fcc804e66a15d1b6fe8ef6e Mon Sep 17 00:00:00 2001 From: muzea Date: Tue, 19 Aug 2025 22:36:07 +0800 Subject: [PATCH 1/2] perf: skipping re-rendering --- src/Filler.tsx | 19 ++++++++++++------- src/List.tsx | 8 +++++--- src/hooks/useChildren.tsx | 33 ++++++++++++++++++--------------- src/hooks/useHeights.tsx | 13 +++++++------ 4 files changed, 42 insertions(+), 31 deletions(-) diff --git a/src/Filler.tsx b/src/Filler.tsx index 5e480e2..f52971b 100644 --- a/src/Filler.tsx +++ b/src/Filler.tsx @@ -1,6 +1,6 @@ -import * as React from 'react'; -import ResizeObserver from 'rc-resize-observer'; import classNames from 'classnames'; +import ResizeObserver from 'rc-resize-observer'; +import * as React from 'react'; export type InnerProps = Pick, 'role' | 'id'>; @@ -69,14 +69,19 @@ const Filler = React.forwardRef( }; } + const handleResize = React.useCallback( + ({ offsetHeight }) => { + if (offsetHeight && onInnerResize) { + onInnerResize(); + } + }, + [onInnerResize], + ); + return (
{ - if (offsetHeight && onInnerResize) { - onInnerResize(); - } - }} + onResize={handleResize} >
(props: ListProps, ref: React.Ref) { setScrollMoving(false); }; - const sharedConfig: SharedConfig = { - getKey, - }; + const sharedConfig: SharedConfig = React.useMemo(() => { + return { + getKey, + }; + }, [getKey]); // ================================ Scroll ================================ function syncScrollTop(newTop: number | ((prev: number) => number)) { diff --git a/src/hooks/useChildren.tsx b/src/hooks/useChildren.tsx index 8c4fdf6..00bc221 100644 --- a/src/hooks/useChildren.tsx +++ b/src/hooks/useChildren.tsx @@ -12,20 +12,23 @@ export default function useChildren( renderFunc: RenderFunc, { getKey }: SharedConfig, ) { - return list.slice(startIndex, endIndex + 1).map((item, index) => { - const eleIndex = startIndex + index; - const node = renderFunc(item, eleIndex, { - style: { - width: scrollWidth, - }, - offsetX, - }) as React.ReactElement; + // 可能存在 list 不变但是里面的数据存在变化的情况,会与之前写法存在不同的行为 + return React.useMemo(() => { + return list.slice(startIndex, endIndex + 1).map((item, index) => { + const eleIndex = startIndex + index; + const node = renderFunc(item, eleIndex, { + style: { + width: scrollWidth, + }, + offsetX, + }) as React.ReactElement; - const key = getKey(item); - return ( - setNodeRef(item, ele)}> - {node} - - ); - }); + const key = getKey(item); + return ( + setNodeRef(item, ele)}> + {node} + + ); + }); + }, [list, startIndex, endIndex, setNodeRef, renderFunc, getKey, offsetX, scrollWidth]); } diff --git a/src/hooks/useHeights.tsx b/src/hooks/useHeights.tsx index ed13de7..bd7865a 100644 --- a/src/hooks/useHeights.tsx +++ b/src/hooks/useHeights.tsx @@ -24,11 +24,11 @@ export default function useHeights( const promiseIdRef = useRef(0); - function cancelRaf() { + const cancelRaf = React.useCallback(function cancelRaf() { promiseIdRef.current += 1; - } + }, []); - function collectHeight(sync = false) { + const collectHeight = React.useCallback(function (sync = false) { cancelRaf(); const doCollect = () => { @@ -67,9 +67,9 @@ export default function useHeights( } }); } - } + }, [cancelRaf]); - function setInstanceRef(item: T, instance: HTMLElement) { + const setInstanceRef = React.useCallback(function setInstanceRef(item: T, instance: HTMLElement) { const key = getKey(item); const origin = instanceRef.current.get(key); @@ -88,11 +88,12 @@ export default function useHeights( onItemRemove?.(item); } } - } + }, [collectHeight, getKey, onItemAdd, onItemRemove]); useEffect(() => { return cancelRaf; }, []); + // 这里稍显迷惑性,当 heightsRef.current.set 被调用时,updatedMark 会变化,进而导致 heightsRef.current 也出现变化 return [setInstanceRef, collectHeight, heightsRef.current, updatedMark]; } From c973f19ff19ac390be57c42d4fe84ae5db34bb1a Mon Sep 17 00:00:00 2001 From: muzea Date: Wed, 20 Aug 2025 22:59:27 +0800 Subject: [PATCH 2/2] fix: some test error --- tests/scroll-Firefox.test.js | 12 +++-- tests/scroll.test.js | 31 ++++++++---- tests/scrollWidth.test.tsx | 8 +++- tests/touch.test.js | 92 ++++++++++++++++++++---------------- 4 files changed, 84 insertions(+), 59 deletions(-) diff --git a/tests/scroll-Firefox.test.js b/tests/scroll-Firefox.test.js index 2a9290e..e0ecc74 100644 --- a/tests/scroll-Firefox.test.js +++ b/tests/scroll-Firefox.test.js @@ -1,9 +1,9 @@ -import React from 'react'; -import { act } from 'react-dom/test-utils'; +import { act } from '@testing-library/react'; import { mount } from 'enzyme'; -import { spyElementPrototypes } from './utils/domHook'; +import React from 'react'; import List from '../src'; import isFF from '../src/utils/isFirefox'; +import { spyElementPrototypes } from './utils/domHook'; function genData(count) { return new Array(count).fill(null).map((_, index) => ({ id: String(index) })); @@ -124,8 +124,10 @@ describe('List.Firefox-Scroll', () => { const wrapper = genList({ itemHeight: 20, height: 100, data: genData(100), ref: listRef }); const ulElement = wrapper.find('ul').instance(); // scroll to bottom - listRef.current.scrollTo(99999); - jest.runAllTimers(); + act(() => { + listRef.current.scrollTo(99999); + jest.runAllTimers(); + }); expect(wrapper.find('ul').instance().scrollTop).toEqual(1900); act(() => { diff --git a/tests/scroll.test.js b/tests/scroll.test.js index e3a7e04..b6ee834 100644 --- a/tests/scroll.test.js +++ b/tests/scroll.test.js @@ -94,9 +94,13 @@ describe('List.Scroll', () => { jest.useFakeTimers(); const listRef = React.createRef(); const wrapper = genList({ itemHeight: 20, height: 100, data: genData(100), ref: listRef }); - jest.runAllTimers(); - listRef.current.scrollTo(null); + act(() => { + jest.runAllTimers(); + + listRef.current.scrollTo(null); + }); + expect(wrapper.find('.rc-virtual-list-scrollbar-thumb').props().style.display).not.toEqual( 'none', ); @@ -107,8 +111,10 @@ describe('List.Scroll', () => { it('value scroll', () => { const listRef = React.createRef(); const wrapper = genList({ itemHeight: 20, height: 100, data: genData(100), ref: listRef }); - listRef.current.scrollTo(903); - jest.runAllTimers(); + act(() => { + listRef.current.scrollTo(903); + jest.runAllTimers(); + }); expect(wrapper.find('ul').instance().scrollTop).toEqual(903); wrapper.unmount(); @@ -125,9 +131,8 @@ describe('List.Scroll', () => { ...result, ref, scrollTo: (...args) => { - ref.current.scrollTo(...args); - act(() => { + ref.current.scrollTo(...args); jest.runAllTimers(); }); }, @@ -153,8 +158,10 @@ describe('List.Scroll', () => { it('scroll top should not out of range', () => { const { scrollTo, container } = presetList(); - scrollTo({ index: 0, align: 'bottom' }); - jest.runAllTimers(); + act(() => { + scrollTo({ index: 0, align: 'bottom' }); + jest.runAllTimers(); + }); expect(container.querySelector('ul').scrollTop).toEqual(0); }); @@ -389,9 +396,13 @@ describe('List.Scroll', () => { ref: listRef, direction: 'rtl', }); - jest.runAllTimers(); - listRef.current.scrollTo(null); + act(() => { + jest.runAllTimers(); + + listRef.current.scrollTo(null); + }); + expect(wrapper.find('.rc-virtual-list-scrollbar-thumb').props().style.display).not.toEqual( 'none', ); diff --git a/tests/scrollWidth.test.tsx b/tests/scrollWidth.test.tsx index 79a141c..094f40d 100644 --- a/tests/scrollWidth.test.tsx +++ b/tests/scrollWidth.test.tsx @@ -230,10 +230,14 @@ describe('List.scrollWidth', () => { ref: listRef, }); - listRef.current.scrollTo({ left: 135 }); + act(() => { + listRef.current.scrollTo({ left: 135 }); + }); expect(listRef.current.getScrollInfo()).toEqual({ x: 135, y: 0 }); - listRef.current.scrollTo({ left: -99 }); + act(() => { + listRef.current.scrollTo({ left: -99 }); + }); expect(listRef.current.getScrollInfo()).toEqual({ x: 0, y: 0 }); }); diff --git a/tests/touch.test.js b/tests/touch.test.js index fa62a6a..3c6e264 100644 --- a/tests/touch.test.js +++ b/tests/touch.test.js @@ -71,22 +71,24 @@ describe('List.Touch', () => { return wrapper.find('.rc-virtual-list-holder').instance(); } - // start - const touchEvent = new Event('touchstart'); - touchEvent.touches = [{ pageY: 100 }]; - getElement().dispatchEvent(touchEvent); - - // move - const moveEvent = new Event('touchmove'); - moveEvent.touches = [{ pageY: 90 }]; - getElement().dispatchEvent(moveEvent); - - // end - const endEvent = new Event('touchend'); - getElement().dispatchEvent(endEvent); - - // smooth - jest.runAllTimers(); + act(() => { + // start + const touchEvent = new Event('touchstart'); + touchEvent.touches = [{ pageY: 100 }]; + getElement().dispatchEvent(touchEvent); + + // move + const moveEvent = new Event('touchmove'); + moveEvent.touches = [{ pageY: 90 }]; + getElement().dispatchEvent(moveEvent); + + // end + const endEvent = new Event('touchend'); + getElement().dispatchEvent(endEvent); + + // smooth + jest.runAllTimers(); + }); expect(wrapper.find('ul').instance().scrollTop > 10).toBeTruthy(); wrapper.unmount(); @@ -99,35 +101,39 @@ describe('List.Touch', () => { return wrapper.find('.rc-virtual-list-holder').instance(); } - // start - const touchEvent = new Event('touchstart'); - touchEvent.touches = [{ pageY: 500 }]; - getElement().dispatchEvent(touchEvent); - - // move const preventDefault = jest.fn(); - const moveEvent = new Event('touchmove'); - moveEvent.touches = [{ pageY: 0 }]; - moveEvent.preventDefault = preventDefault; - getElement().dispatchEvent(moveEvent); + act(() => { + // start + const touchEvent = new Event('touchstart'); + touchEvent.touches = [{ pageY: 500 }]; + getElement().dispatchEvent(touchEvent); + + // move + const moveEvent = new Event('touchmove'); + moveEvent.touches = [{ pageY: 0 }]; + moveEvent.preventDefault = preventDefault; + getElement().dispatchEvent(moveEvent); + }); // Call preventDefault expect(preventDefault).toHaveBeenCalled(); - // ======= Not call since scroll to the bottom ======= - jest.runAllTimers(); - preventDefault.mockReset(); + act(() => { + // ======= Not call since scroll to the bottom ======= + jest.runAllTimers(); + preventDefault.mockReset(); - // start - const touchEvent2 = new Event('touchstart'); - touchEvent2.touches = [{ pageY: 500 }]; - getElement().dispatchEvent(touchEvent2); + // start + const touchEvent2 = new Event('touchstart'); + touchEvent2.touches = [{ pageY: 500 }]; + getElement().dispatchEvent(touchEvent2); - // move - const moveEvent2 = new Event('touchmove'); - moveEvent2.touches = [{ pageY: 0 }]; - moveEvent2.preventDefault = preventDefault; - getElement().dispatchEvent(moveEvent2); + // move + const moveEvent2 = new Event('touchmove'); + moveEvent2.touches = [{ pageY: 0 }]; + moveEvent2.preventDefault = preventDefault; + getElement().dispatchEvent(moveEvent2); + }); expect(preventDefault).not.toHaveBeenCalled(); }); @@ -137,16 +143,18 @@ describe('List.Touch', () => { const preventDefault = jest.fn(); const wrapper = genList({ itemHeight: 20, height: 100, data: genData(100) }); - const touchEvent = new Event('touchstart'); - touchEvent.preventDefault = preventDefault; - wrapper.find('.rc-virtual-list-scrollbar').instance().dispatchEvent(touchEvent); + act(() => { + const touchEvent = new Event('touchstart'); + touchEvent.preventDefault = preventDefault; + wrapper.find('.rc-virtual-list-scrollbar').instance().dispatchEvent(touchEvent); + }); expect(preventDefault).toHaveBeenCalled(); }); it('nest touch', async () => { const { container } = render( - + {({ id }) => id === '0' ? (