Skip to content

Commit f97e46e

Browse files
authored
refactor: Use frame of scroll (#44)
* clean up ref * add update mark * rm ref cache * wheel as a frame * fix test case * coverage
1 parent bd63f9a commit f97e46e

File tree

9 files changed

+97
-49
lines changed

9 files changed

+97
-49
lines changed

src/Item.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import * as React from 'react';
2+
3+
export interface ItemProps {
4+
children: React.ReactElement;
5+
setRef: (element: HTMLElement) => void;
6+
}
7+
8+
export function Item({ children, setRef }: ItemProps) {
9+
const refFunc = React.useCallback(node => {
10+
setRef(node);
11+
}, []);
12+
13+
return React.cloneElement(children, {
14+
ref: refFunc,
15+
});
16+
}

src/List.tsx

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import useHeights from './hooks/useHeights';
88
import useInRange from './hooks/useInRange';
99
import useScrollTo from './hooks/useScrollTo';
1010
import useDiffItem from './hooks/useDiffItem';
11+
import useFrameWheel from './hooks/useFrameWheel';
1112

1213
const EMPTY_DATA = [];
1314

@@ -97,7 +98,7 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
9798
diffItemRef.current = diffItem;
9899

99100
// ================================ Height ================================
100-
const [getInstanceRefFunc, collectHeight, heights, heightUpdatedMark] = useHeights(
101+
const [setInstanceRef, collectHeight, heights, heightUpdatedMark] = useHeights(
101102
getKey,
102103
null,
103104
null,
@@ -169,19 +170,14 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
169170

170171
// ================================ Scroll ================================
171172
// Since this added in global,should use ref to keep update
172-
function onRawWheel(event: MouseWheelEvent) {
173-
if (!inVirtual) return;
174-
175-
// Proxy of scroll events
176-
event.preventDefault();
177-
173+
const onRawWheel = useFrameWheel(inVirtual, offsetY => {
178174
setScrollTop(top => {
179-
const newTop = keepInRange(top + event.deltaY);
175+
const newTop = keepInRange(top + offsetY);
180176

181177
componentRef.current.scrollTop = newTop;
182178
return newTop;
183179
});
184-
}
180+
});
185181

186182
// Additional handle the scroll which not trigger by wheel
187183
function onRawScroll(event: React.UIEvent) {
@@ -214,14 +210,7 @@ export function RawList<T>(props: ListProps<T>, ref: React.Ref<ListRef>) {
214210
}));
215211

216212
// ================================ Render ================================
217-
const listChildren = useChildren(
218-
mergedData,
219-
start,
220-
end,
221-
getInstanceRefFunc,
222-
children,
223-
sharedConfig,
224-
);
213+
const listChildren = useChildren(mergedData, start, end, setInstanceRef, children, sharedConfig);
225214

226215
return (
227216
<Component

src/hooks/useChildren.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import * as React from 'react';
22
import { SharedConfig, RenderFunc } from '../interface';
3+
import { Item } from '../Item';
34

45
export default function useChildren<T>(
56
list: T[],
67
startIndex: number,
78
endIndex: number,
8-
getInstanceRefFunc: (item: T) => (instance: HTMLElement) => void,
9+
setNodeRef: (item: T, element: HTMLElement) => void,
910
renderFunc: RenderFunc<T>,
1011
{ getKey }: SharedConfig<T>,
1112
) {
@@ -16,9 +17,10 @@ export default function useChildren<T>(
1617
}) as React.ReactElement;
1718

1819
const key = getKey(item);
19-
return React.cloneElement(node, {
20-
key,
21-
ref: getInstanceRefFunc(item),
22-
});
20+
return (
21+
<Item key={key} setRef={ele => setNodeRef(item, ele)}>
22+
{node}
23+
</Item>
24+
);
2325
});
2426
}

src/hooks/useFrameWheel.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { useRef } from 'react';
2+
import raf from 'rc-util/lib/raf';
3+
4+
export default function useFrameWheel(inVirtual: boolean, onWheelDelta: (offset: number) => void) {
5+
const offsetRef = useRef(0);
6+
const nextFrameRef = useRef<number>(null);
7+
8+
function onWheel(event: MouseWheelEvent) {
9+
if (!inVirtual) return;
10+
11+
// Proxy of scroll events
12+
event.preventDefault();
13+
14+
raf.cancel(nextFrameRef.current);
15+
offsetRef.current += event.deltaY;
16+
17+
nextFrameRef.current = raf(() => {
18+
onWheelDelta(offsetRef.current);
19+
offsetRef.current = 0;
20+
});
21+
}
22+
23+
return onWheel;
24+
}

src/hooks/useHeights.tsx

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,41 +10,48 @@ export default function useHeights<T>(
1010
getKey: GetKey<T>,
1111
onItemAdd?: (item: T) => void,
1212
onItemRemove?: (item: T) => void,
13-
): [(item: T) => (instance: HTMLElement) => void, () => void, CacheMap, number] {
13+
): [(item: T, instance: HTMLElement) => void, () => void, CacheMap, number] {
1414
const [updatedMark, setUpdatedMark] = React.useState(0);
1515
const instanceRef = useRef(new Map<React.Key, HTMLElement>());
1616
const heightsRef = useRef(new CacheMap());
1717

18-
const instanceFuncRef = useRef<Map<React.Key, RefFunc>>(new Map());
19-
function getInstanceRefFunc(item: T) {
18+
function setInstanceRef(item: T, instance: HTMLElement) {
2019
const key = getKey(item);
21-
if (!instanceFuncRef.current.has(key)) {
22-
instanceFuncRef.current.set(key, (instance: HTMLElement) => {
23-
const origin = instanceRef.current.get(key);
24-
instanceRef.current.set(key, instance);
25-
26-
// Instance changed
27-
if (!origin !== !instance) {
28-
if (instance) {
29-
onItemAdd?.(item);
30-
} else {
31-
onItemRemove?.(item);
32-
}
33-
}
34-
});
20+
const origin = instanceRef.current.get(key);
21+
22+
if (instance) {
23+
instanceRef.current.set(key, instance);
24+
} else {
25+
instanceRef.current.delete(key);
26+
}
27+
28+
// Instance changed
29+
if (!origin !== !instance) {
30+
if (instance) {
31+
onItemAdd?.(item);
32+
} else {
33+
onItemRemove?.(item);
34+
}
3535
}
36-
return instanceFuncRef.current.get(key);
3736
}
3837

3938
function collectHeight() {
39+
let changed = false;
40+
4041
instanceRef.current.forEach((element, key) => {
4142
if (element && element.offsetParent) {
4243
const htmlElement = findDOMNode<HTMLElement>(element);
43-
heightsRef.current.set(key, htmlElement.offsetHeight);
44+
const { offsetHeight } = htmlElement;
45+
if (heightsRef.current.get(key) !== offsetHeight) {
46+
changed = true;
47+
heightsRef.current.set(key, htmlElement.offsetHeight);
48+
}
4449
}
4550
});
46-
setUpdatedMark(c => c + 1);
51+
if (changed) {
52+
setUpdatedMark(c => c + 1);
53+
}
4754
}
4855

49-
return [getInstanceRefFunc, collectHeight, heightsRef.current, updatedMark];
56+
return [setInstanceRef, collectHeight, heightsRef.current, updatedMark];
5057
}

tests/list.test.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,16 @@ describe('List.Basic', () => {
192192
const wrapper = genList({ itemHeight: 20, height: 40, data: genData(3) });
193193
wrapper
194194
.find('Filler')
195+
.find('ResizeObserver')
195196
.props()
196-
.onInnerResize();
197+
.onResize({ offsetHeight: 0 });
198+
expect(collected).toBeFalsy();
199+
200+
wrapper
201+
.find('Filler')
202+
.find('ResizeObserver')
203+
.props()
204+
.onResize({ offsetHeight: 100 });
197205
expect(collected).toBeTruthy();
198206
});
199207
});

tests/mock.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ describe('MockList', () => {
1616
for (let i = 0; i < 3; i += 1) {
1717
expect(
1818
wrapper
19-
.find('span')
19+
.find('Item')
2020
.at(i)
2121
.key(),
2222
).toBe(String(i));

tests/props.test.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,28 @@ import List from '../src';
44

55
describe('Props', () => {
66
it('itemKey is a function', () => {
7-
class Item extends React.Component {
7+
class ItemComponent extends React.Component {
88
render() {
99
return this.props.children;
1010
}
1111
}
1212

1313
const wrapper = mount(
1414
<List data={[{ id: 903 }, { id: 1128 }]} itemKey={item => item.id}>
15-
{({ id }) => <Item>{id}</Item>}
15+
{({ id }) => <ItemComponent>{id}</ItemComponent>}
1616
</List>,
1717
);
1818

1919
expect(
2020
wrapper
21-
.find(Item)
21+
.find('Item')
2222
.at(0)
2323
.key(),
2424
).toBe('903');
2525

2626
expect(
2727
wrapper
28-
.find(Item)
28+
.find('Item')
2929
.at(1)
3030
.key(),
3131
).toBe('1128');

tests/scroll.test.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ describe('List.Scroll', () => {
9696
wheelEvent.preventDefault = preventDefault;
9797
ulElement.dispatchEvent(wheelEvent);
9898

99+
jest.runAllTimers();
100+
99101
expect(preventDefault).toHaveBeenCalled();
100102
});
101103
});

0 commit comments

Comments
 (0)