-
-
Notifications
You must be signed in to change notification settings - Fork 620
fix: prevent memory leaks in table and sticky scroll components #1288
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -43,6 +43,8 @@ | |
| }); | ||
| const [isActive, setActive] = React.useState(false); | ||
| const rafRef = React.useRef<number | null>(null); | ||
| // 记录上一次的 scrollParents | ||
| const lastScrollParentsRef = React.useRef<(HTMLElement | SVGElement)[]>([]); | ||
|
|
||
| React.useEffect( | ||
| () => () => { | ||
|
|
@@ -149,14 +151,22 @@ | |
|
|
||
| // Loop for scroll event check | ||
| React.useEffect(() => { | ||
| if (!scrollBodyRef.current) return; | ||
| if (!scrollBodyRef.current) { | ||
| return; | ||
| } | ||
|
|
||
| // 清理上一次 scrollParents 的事件监听 | ||
| lastScrollParentsRef.current.forEach(p => | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 这个看起来应该是在 effect 的 cleanup 里直接做,不用额外做个 lastScrollParentsRef 来存之前的内容。直接闭包存就够了
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| p.removeEventListener('scroll', checkScrollBarVisible), | ||
| ); | ||
|
|
||
| const scrollParents: (HTMLElement | SVGElement)[] = []; | ||
| let parent = getDOM(scrollBodyRef.current); | ||
| while (parent) { | ||
| scrollParents.push(parent); | ||
| parent = parent.parentElement; | ||
| } | ||
| lastScrollParentsRef.current = scrollParents; | ||
|
|
||
| scrollParents.forEach(p => p.addEventListener('scroll', checkScrollBarVisible, false)); | ||
| window.addEventListener('resize', checkScrollBarVisible, false); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,7 +6,7 @@ import Table, { INTERNAL_COL_DEFINE } from '../src'; | |
| import BodyRow from '../src/Body/BodyRow'; | ||
| import Cell from '../src/Cell'; | ||
| import { INTERNAL_HOOKS } from '../src/constant'; | ||
| import { fireEvent, render } from '@testing-library/react'; | ||
| import { fireEvent, render, cleanup } from '@testing-library/react'; | ||
|
|
||
| describe('Table.Basic', () => { | ||
| const data = [ | ||
|
|
@@ -1372,3 +1372,73 @@ describe('Table.Basic', () => { | |
| expect(onScroll).toHaveBeenCalled(); | ||
| }); | ||
| }); | ||
|
|
||
| describe('Table memory leak and cleanup', () => { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 这个测试发现在原来分支跑也是过的 |
||
| afterEach(() => { | ||
| cleanup(); | ||
| vi.clearAllTimers(); | ||
| }); | ||
|
|
||
| it('should cleanup stickyScrollBar events on unmount', () => { | ||
| const addEventListenerSpy = vi.spyOn(window, 'addEventListener'); | ||
| const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener'); | ||
| const { unmount } = render( | ||
| <Table | ||
| columns={[{ title: 'A', dataIndex: 'a' }]} | ||
| data={[{ a: 1 }]} | ||
| scroll={{ x: 100 }} | ||
| sticky | ||
| />, | ||
| ); | ||
| unmount(); | ||
| // 断言事件被移除 | ||
| expect(removeEventListenerSpy).toHaveBeenCalledWith('scroll', expect.any(Function)); | ||
| expect(removeEventListenerSpy).toHaveBeenCalledWith('resize', expect.any(Function)); | ||
| addEventListenerSpy.mockRestore(); | ||
| removeEventListenerSpy.mockRestore(); | ||
| }); | ||
|
|
||
| it('should not call setTimeout callback after unmount', () => { | ||
| vi.useFakeTimers(); | ||
| const { unmount } = render( | ||
| <Table columns={[{ title: 'A', dataIndex: 'a' }]} data={[{ a: 1 }]} scroll={{ x: 100 }} />, | ||
| ); | ||
| unmount(); | ||
| // 触发所有定时器 | ||
| vi.runAllTimers(); | ||
| // 没有报错即通过 | ||
| vi.useRealTimers(); | ||
| }); | ||
|
|
||
| it('should not leak when mount/unmount multiple times', () => { | ||
| for (let i = 0; i < 5; i++) { | ||
| const { unmount } = render( | ||
| <Table columns={[{ title: 'A', dataIndex: 'a' }]} data={[{ a: 1 }]} scroll={{ x: 100 }} />, | ||
| ); | ||
| unmount(); | ||
| } | ||
| // 没有报错即通过 | ||
| }); | ||
|
|
||
| it('should cleanup scrollParents events when scrollBodyRef changes', () => { | ||
| // 这里只能间接测试,mount/unmount 后事件应被清理 | ||
| const addEventListenerSpy = vi.spyOn(window, 'addEventListener'); | ||
| const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener'); | ||
| const { rerender, unmount } = render( | ||
| <Table | ||
| columns={[{ title: 'A', dataIndex: 'a' }]} | ||
| data={[{ a: 1 }]} | ||
| scroll={{ x: 100 }} | ||
| sticky | ||
| />, | ||
| ); | ||
| // 模拟数据变化导致 scrollBodyRef 变化 | ||
| rerender( | ||
| <Table columns={[{ title: 'A', dataIndex: 'a' }]} data={[]} scroll={{ x: 100 }} sticky />, | ||
| ); | ||
| unmount(); | ||
| expect(removeEventListenerSpy).toHaveBeenCalledWith('scroll', expect.any(Function)); | ||
| addEventListenerSpy.mockRestore(); | ||
| removeEventListenerSpy.mockRestore(); | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这里没括号原来是为了满足覆盖率啊 😂