Skip to content

Commit 4e64b55

Browse files
authored
fix: hover perf (#842)
* fix: hover perf * test: add test case
1 parent b3a76da commit 4e64b55

File tree

7 files changed

+172
-12
lines changed

7 files changed

+172
-12
lines changed

docs/demo/jsx.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
## jsx
1+
## Hover Perf
22

3-
<code src="../examples/jsx.tsx">
3+
<code src="../examples/hover-perf.tsx">

docs/examples/hover-perf.tsx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import React from 'react';
2+
import Table from 'rc-table';
3+
import '../../assets/index.less';
4+
5+
const CellExample = ({ data, count }) => {
6+
console.log('rerender ' + Date.now());
7+
return <>{count + ' ' + data.index}</>;
8+
};
9+
10+
// const dataSource = Array.from({ length: 10000 }).map((_, index) => ({ index }));
11+
const dataSource = Array.from({ length: 100 }).map((_, index) => ({ index, key: index }));
12+
13+
const ProblemTable = () => {
14+
const columns = [
15+
{
16+
title: 'Grouped by 10',
17+
onCell: (_, index) => ({ rowSpan: index % 10 === 0 ? 10 : 0 }),
18+
render: (_, record) => (
19+
<span>
20+
{record.index}-{record.index + 10}
21+
</span>
22+
),
23+
},
24+
{
25+
title: 'one',
26+
render: (_, record) => <CellExample count="one" data={record} />,
27+
},
28+
{
29+
title: 'two',
30+
render: (_, record) => <CellExample count="two" data={record} />,
31+
},
32+
{
33+
title: 'three',
34+
render: (_, record) => <CellExample count="three" data={record} />,
35+
},
36+
{
37+
title: 'four',
38+
render: (_, record) => <CellExample count="four" data={record} />,
39+
},
40+
];
41+
return <Table tableLayout="fixed" data={dataSource} columns={columns} />;
42+
};
43+
44+
export default ProblemTable;

src/Body/index.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,6 @@ function Body<RecordType>({
5454
setEndRow(end);
5555
}, []);
5656

57-
const hoverContext = React.useMemo(
58-
() => ({ startRow, endRow, onHover }),
59-
[onHover, startRow, endRow],
60-
);
61-
6257
// ====================== Render ======================
6358
const bodyNode = React.useMemo(() => {
6459
const WrapperComponent = getComponent(['body', 'wrapper'], 'tbody');
@@ -141,7 +136,9 @@ function Body<RecordType>({
141136

142137
return (
143138
<PerfContext.Provider value={perfRef.current}>
144-
<HoverContext.Provider value={hoverContext}>{bodyNode}</HoverContext.Provider>
139+
<HoverContext.Provider value={{ startRow, endRow, onHover }}>
140+
{bodyNode}
141+
</HoverContext.Provider>
145142
</PerfContext.Provider>
146143
);
147144
}

src/Cell/index.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import HoverContext from '../context/HoverContext';
1818
import type { HoverContextProps } from '../context/HoverContext';
1919
import warning from 'rc-util/lib/warning';
2020
import PerfContext from '../context/PerfContext';
21+
import { useContextSelector } from '../ContextSelector';
2122

2223
/** Check if cell is in hover range */
2324
function inHoverRange(cellStartRow: number, cellRowSpan: number, startRow: number, endRow: number) {
@@ -327,14 +328,20 @@ const MemoCell = React.memo(
327328

328329
/** Inject hover data here, we still wish MemoCell keep simple `shouldCellUpdate` logic */
329330
const WrappedCell = React.forwardRef((props: CellProps<any>, ref: React.Ref<any>) => {
330-
const { onHover, startRow, endRow } = React.useContext(HoverContext);
331331
const { index, additionalProps = {}, colSpan, rowSpan } = props;
332332
const { colSpan: cellColSpan, rowSpan: cellRowSpan } = additionalProps;
333333

334334
const mergedColSpan = colSpan ?? cellColSpan;
335335
const mergedRowSpan = rowSpan ?? cellRowSpan;
336336

337-
const hovering = inHoverRange(index, mergedRowSpan || 1, startRow, endRow);
337+
const { onHover, hovering } = useContextSelector(HoverContext, cxt => {
338+
const isHovering = inHoverRange(index, mergedRowSpan || 1, cxt?.startRow, cxt?.endRow);
339+
340+
return {
341+
onHover: cxt?.onHover,
342+
hovering: isHovering,
343+
};
344+
});
338345

339346
return (
340347
<MemoCell

src/ContextSelector/index.tsx

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import * as React from 'react';
2+
import useLayoutEffect from 'rc-util/lib/hooks/useLayoutEffect';
3+
import useEvent from 'rc-util/lib/hooks/useEvent';
4+
import shallowEqual from 'shallowequal';
5+
6+
export type Selector<T, O = T> = (value: T) => O;
7+
8+
export type Trigger<T> = (value: T) => void;
9+
10+
export type Listeners<T> = Set<Trigger<T>>;
11+
12+
export interface Context<T> {
13+
getValue: () => T;
14+
listeners: Listeners<T>;
15+
}
16+
17+
export interface ContextSelectorProviderProps<T> {
18+
value: T;
19+
children?: React.ReactNode;
20+
}
21+
22+
export interface ReturnCreateContext<T> {
23+
Context: React.Context<Context<T>>;
24+
Provider: React.ComponentType<ContextSelectorProviderProps<T>>;
25+
}
26+
27+
export function createContext<T>(): ReturnCreateContext<T> {
28+
const Context = React.createContext<Context<T>>(null as any);
29+
30+
const Provider = ({ value, children }: ContextSelectorProviderProps<T>) => {
31+
const valueRef = React.useRef(value);
32+
valueRef.current = value;
33+
34+
const [context] = React.useState<Context<T>>(() => ({
35+
getValue: () => valueRef.current,
36+
listeners: new Set(),
37+
}));
38+
39+
useLayoutEffect(() => {
40+
context.listeners.forEach(listener => {
41+
listener(value);
42+
});
43+
}, [value]);
44+
45+
return <Context.Provider value={context}>{children}</Context.Provider>;
46+
};
47+
48+
return { Context, Provider };
49+
}
50+
51+
export function useContextSelector<T, O>(holder: ReturnCreateContext<T>, selector: Selector<T, O>) {
52+
const eventSelector = useEvent(selector);
53+
const context = React.useContext(holder?.Context);
54+
const { listeners, getValue } = context || {};
55+
56+
const [value, setValue] = React.useState(() => eventSelector(context ? getValue() : null));
57+
58+
React.useLayoutEffect(() => {
59+
if (!context) {
60+
return;
61+
}
62+
63+
function trigger(nextValue: T) {
64+
setValue(prev => {
65+
const selectedValue = eventSelector(nextValue);
66+
return shallowEqual(prev, selectedValue) ? prev : selectedValue;
67+
});
68+
}
69+
70+
listeners.add(trigger);
71+
72+
return () => {
73+
listeners.delete(trigger);
74+
};
75+
}, [context]);
76+
77+
return value;
78+
}

src/context/HoverContext.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import * as React from 'react';
1+
import { createContext } from '../ContextSelector';
22

33
export interface HoverContextProps {
44
startRow: number;
55
endRow: number;
66
onHover: (start: number, end: number) => void;
77
}
88

9-
const HoverContext = React.createContext<HoverContextProps>({} as any);
9+
const HoverContext = createContext<HoverContextProps>();
1010

1111
export default HoverContext;

tests/Hover.spec.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from 'react';
22
import { mount } from 'enzyme';
33
import { resetWarned } from 'rc-util/lib/warning';
4+
import toArray from 'rc-util/lib/Children/toArray';
45
import Table from '../src';
56
import type { TableProps } from '../src/Table';
67

@@ -202,4 +203,37 @@ describe('Table.Hover', () => {
202203
expect(renderTimes).toBe(0);
203204
});
204205
});
206+
207+
it('perf', () => {
208+
const renderTimes = {};
209+
210+
const TD = (props: any) => {
211+
const children = toArray(props.children);
212+
const first = children[0] as unknown as string;
213+
214+
renderTimes[first] = (renderTimes[first] || 0) + 1;
215+
return <td {...props} />;
216+
};
217+
218+
const wrapper = mount(
219+
createTable({
220+
components: {
221+
body: {
222+
cell: TD,
223+
},
224+
},
225+
}),
226+
);
227+
228+
const firstMountTimes = renderTimes.Jack;
229+
230+
wrapper.find('tbody td').first().simulate('mouseEnter');
231+
expect(wrapper.exists('.rc-table-cell-row-hover')).toBeTruthy();
232+
233+
wrapper.find('tbody td').first().simulate('mouseLeave');
234+
expect(wrapper.exists('.rc-table-cell-row-hover')).toBeFalsy();
235+
236+
expect(firstMountTimes).toEqual(renderTimes.Jack);
237+
expect(renderTimes.Lucy).toBeGreaterThan(renderTimes.Jack);
238+
});
205239
});

0 commit comments

Comments
 (0)