Skip to content

Commit dae3962

Browse files
authored
Merge pull request #845 from thundersdata-frontend/rn-issue
fix: 优化picker组件的性能问题
2 parents 0b8bb2f + a1ca23a commit dae3962

File tree

8 files changed

+150
-110
lines changed

8 files changed

+150
-110
lines changed

.changeset/big-cheetahs-smell.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@td-design/react-native-picker': minor
3+
---
4+
5+
fix: 优化picker组件的性能问题

packages/react-native-picker/src/components/DatePicker/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ const DatePicker: FC<
3232
{...restProps}
3333
data={col}
3434
value={values[index]}
35-
onChange={itemValue => onValueChange(itemValue, index)}
35+
index={index}
36+
onChange={onValueChange}
3637
/>
3738
);
3839
})}

packages/react-native-picker/src/components/WheelPicker/index.tsx

Lines changed: 49 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
1-
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
2-
import { Animated, FlatList, NativeScrollEvent, NativeSyntheticEvent, StyleSheet, View } from 'react-native';
1+
import React, { useEffect, useMemo, useRef } from 'react';
2+
import {
3+
Animated,
4+
FlatList,
5+
ListRenderItemInfo,
6+
NativeScrollEvent,
7+
NativeSyntheticEvent,
8+
StyleSheet,
9+
View,
10+
} from 'react-native';
311

412
import { Theme, useTheme } from '@td-design/react-native';
13+
import { useMemoizedFn } from '@td-design/rn-hooks';
514

6-
import { WheelPickerProps } from './type';
15+
import { OptionItem, WheelPickerProps } from './type';
716
import WheelPickerItem from './WheelPickerItem';
817

918
export default function WheelPicker({
@@ -14,6 +23,7 @@ export default function WheelPicker({
1423
itemStyle,
1524
itemTextStyle,
1625
itemHeight = 40,
26+
index,
1727
onChange,
1828
}: WheelPickerProps) {
1929
const theme = useTheme<Theme>();
@@ -23,56 +33,44 @@ export default function WheelPicker({
2333

2434
const containerHeight = 5 * itemHeight;
2535

26-
const paddedOptions = useMemo(() => {
36+
const { paddedOptions, offsets } = useMemo(() => {
2737
const array = [...data];
2838
for (let i = 0; i < 2; i++) {
2939
array.unshift(undefined);
3040
array.push(undefined);
3141
}
32-
return array;
33-
}, [data]);
42+
return {
43+
paddedOptions: array,
44+
offsets: array.map((_, i) => i * itemHeight),
45+
};
46+
}, [data, itemHeight]);
3447

3548
let selectedIndex = data.findIndex(item => item?.value === value);
3649
if (selectedIndex === -1) {
3750
selectedIndex = 0;
3851
}
3952

40-
const offsets = useMemo(
41-
() => [...Array(paddedOptions.length)].map((_, i) => i * itemHeight),
42-
[paddedOptions, itemHeight]
43-
);
44-
4553
const currentScrollIndex = Animated.add(Animated.divide(scrollY, itemHeight), 2);
4654

47-
const handleMomentumScrollBegin = useCallback(() => {
55+
const handleMomentumScrollBegin = useMemoizedFn(() => {
4856
signal.current = false;
49-
}, []);
50-
51-
const handleMomentumScrollEnd = useCallback(
52-
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
53-
if (signal.current) return;
54-
signal.current = true;
57+
});
5558

56-
// Due to list bounciness when scrolling to the start or the end of the list
57-
// the offset might be negative or over the last item.
58-
// We therefore clamp the offset to the supported range.
59-
const offsetY = Math.min(itemHeight * (data.length - 1), Math.max(event.nativeEvent.contentOffset.y, 0));
60-
let index = Math.ceil(offsetY / itemHeight) + 1;
59+
const handleMomentumScrollEnd = useMemoizedFn((event: NativeSyntheticEvent<NativeScrollEvent>) => {
60+
if (signal.current) return;
61+
signal.current = true;
6162

62-
const currentItem = data[index - 1];
63-
if (currentItem) {
64-
onChange(currentItem.value);
65-
}
66-
},
67-
[itemHeight, data]
68-
);
63+
// Due to list bounciness when scrolling to the start or the end of the list
64+
// the offset might be negative or over the last item.
65+
// We therefore clamp the offset to the supported range.
66+
const offsetY = Math.min(itemHeight * (data.length - 1), Math.max(event.nativeEvent.contentOffset.y, 0));
67+
const _index = Math.ceil(offsetY / itemHeight) + 1;
6968

70-
useEffect(() => {
71-
flatListRef.current?.scrollToIndex({
72-
index: selectedIndex,
73-
animated: false,
74-
});
75-
}, [selectedIndex]);
69+
const currentItem = data[_index - 1];
70+
if (currentItem) {
71+
onChange(currentItem.value, index);
72+
}
73+
});
7674

7775
const styles = StyleSheet.create({
7876
container: {
@@ -94,8 +92,15 @@ export default function WheelPicker({
9492
},
9593
});
9694

97-
const renderItem = useCallback(
98-
({ item: option, index }) => (
95+
useEffect(() => {
96+
flatListRef.current?.scrollToIndex({
97+
index: selectedIndex,
98+
animated: false,
99+
});
100+
}, [selectedIndex]);
101+
102+
const renderItem = useMemoizedFn(({ item: option, index }: ListRenderItemInfo<OptionItem>) => {
103+
return (
99104
<WheelPickerItem
100105
index={index}
101106
option={option}
@@ -105,9 +110,8 @@ export default function WheelPicker({
105110
currentIndex={currentScrollIndex}
106111
visibleRest={2}
107112
/>
108-
),
109-
[itemStyle, itemTextStyle, itemHeight, currentScrollIndex]
110-
);
113+
);
114+
});
111115

112116
return (
113117
<View style={[styles.container, containerStyle]}>
@@ -122,16 +126,18 @@ export default function WheelPicker({
122126
onMomentumScrollBegin={handleMomentumScrollBegin}
123127
onMomentumScrollEnd={handleMomentumScrollEnd}
124128
snapToOffsets={offsets}
125-
decelerationRate={'normal'}
126-
initialScrollIndex={selectedIndex}
129+
decelerationRate={'fast'}
127130
getItemLayout={(_, index) => ({
128131
length: itemHeight,
129132
offset: itemHeight * index,
130133
index,
131134
})}
135+
bounces={false}
132136
data={paddedOptions}
133137
keyExtractor={(_, index) => index.toString()}
134138
renderItem={renderItem}
139+
maxToRenderPerBatch={3}
140+
initialNumToRender={2}
135141
/>
136142
</View>
137143
);

packages/react-native-picker/src/components/WheelPicker/type.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,13 @@ export interface WheelPickerPropsBase {
2525

2626
/** 滚轮选择器的属性 */
2727
export interface WheelPickerProps extends WheelPickerPropsBase {
28+
index: number;
2829
/** 数据行数组 */
2930
data: (CascadePickerItemProps | undefined)[];
3031
/** 当前选中的数据行下标 */
3132
value: ItemValue;
3233
/** 选择数据行的处理函数 */
33-
onChange: (value: ItemValue) => void;
34+
onChange: (value: ItemValue, index: number) => void;
3435
}
3536

3637
/** 滚轮选择器子项的属性 */

packages/react-native-picker/src/date-picker/index.tsx

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { forwardRef, useImperativeHandle } from 'react';
1+
import React, { forwardRef, useImperativeHandle, useMemo } from 'react';
22
import { StyleSheet } from 'react-native';
33

44
import { Flex, helpers, Modal, Pressable, Text } from '@td-design/react-native';
@@ -59,17 +59,21 @@ const DatePicker = forwardRef<DatePickerRef, DatePickerProps>((props, ref) => {
5959
submit: { width: '100%', justifyContent: 'center', alignItems: 'flex-end' },
6060
});
6161

62-
const DatePickerComp = (
63-
<DatePickerRN
64-
{...restProps}
65-
{...{ mode, value: date, minDate, maxDate, labelUnit, format }}
66-
onChange={handleChange}
67-
/>
68-
);
62+
const DatePickerComp = useMemo(() => {
63+
if (!visible) return null;
64+
65+
return (
66+
<DatePickerRN
67+
{...restProps}
68+
{...{ mode, value: date, minDate, maxDate, labelUnit, format }}
69+
onChange={handleChange}
70+
/>
71+
);
72+
}, [visible, date, mode, minDate, maxDate, labelUnit, format, restProps]);
6973

7074
if (displayType === 'modal') {
7175
return (
72-
<Modal visible={visible} onClose={handleClose} animationDuration={150}>
76+
<Modal visible={visible} onClose={handleClose} animationDuration={0}>
7377
<Flex
7478
borderBottomWidth={ONE_PIXEL}
7579
borderBottomColor="border"

packages/react-native-picker/src/picker/components/Cascade/index.tsx

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react';
1+
import React, { useMemo } from 'react';
22
import { StyleSheet } from 'react-native';
33

44
import { Flex, helpers, Modal, Pressable, Text } from '@td-design/react-native';
@@ -32,21 +32,26 @@ const Cascader = ({
3232
onClose,
3333
});
3434

35-
const PickerComp = (
36-
<Flex backgroundColor="white">
37-
{childrenTree.map((item: CascadePickerItemProps[] = [], level) => (
38-
<WheelPicker
39-
key={level}
40-
{...{ data: item.map(el => ({ ...el, value: `${el.value}` })), value: `${stateValue[level]}` }}
41-
onChange={val => handleValueChange(val, level)}
42-
/>
43-
))}
44-
</Flex>
45-
);
35+
const PickerComp = useMemo(() => {
36+
if (!visible) return null;
37+
if (childrenTree.length === 0) return null;
38+
39+
return (
40+
<Flex backgroundColor="white">
41+
{childrenTree.map((item: CascadePickerItemProps[] = [], index) => (
42+
<WheelPicker
43+
key={index}
44+
{...{ data: item.map(el => ({ ...el, value: `${el.value}` })), index, value: `${stateValue[index]}` }}
45+
onChange={handleValueChange}
46+
/>
47+
))}
48+
</Flex>
49+
);
50+
}, [visible, childrenTree, stateValue]);
4651

4752
if (displayType === 'modal') {
4853
return (
49-
<Modal visible={visible} onClose={onClose} animationDuration={150}>
54+
<Modal visible={visible} onClose={onClose} animationDuration={0}>
5055
<Flex
5156
borderBottomWidth={ONE_PIXEL}
5257
borderBottomColor="border"
@@ -86,4 +91,4 @@ const styles = StyleSheet.create({
8691
submit: { width: '100%', justifyContent: 'center', alignItems: 'flex-end' },
8792
});
8893

89-
export default React.memo(Cascader);
94+
export default Cascader;

packages/react-native-picker/src/picker/components/Normal/index.tsx

Lines changed: 59 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { FC } from 'react';
1+
import React, { FC, useMemo } from 'react';
22
import { StyleSheet } from 'react-native';
33

44
import { Flex, helpers, Modal, Pressable, Text } from '@td-design/react-native';
@@ -32,57 +32,75 @@ const NormalPicker: FC<PickerProps & ModalPickerProps> = props => {
3232
displayType,
3333
});
3434

35-
const PickerComp = (
36-
<Flex backgroundColor="white">
37-
{pickerData.map((item, index) => (
38-
<WheelPicker
39-
key={index}
40-
{...restProps}
41-
{...{ data: item, value: selectedValue ? selectedValue[index] : '' }}
42-
onChange={val => handleChange(val, index)}
43-
/>
44-
))}
45-
</Flex>
46-
);
35+
const PickerComp = useMemo(() => {
36+
if (!visible) return null;
37+
if (pickerData.length === 0) return null;
38+
39+
if (pickerData.length === 1)
40+
return (
41+
<Flex backgroundColor="white">
42+
<WheelPicker
43+
{...restProps}
44+
{...{ data: pickerData[0], index: 0, value: selectedValue?.[0] ?? '' }}
45+
onChange={handleChange}
46+
/>
47+
</Flex>
48+
);
49+
50+
return (
51+
<Flex backgroundColor="white">
52+
{pickerData.map((item, index) => (
53+
<WheelPicker
54+
key={index}
55+
{...restProps}
56+
{...{ data: item, index, value: selectedValue?.[index] ?? '' }}
57+
onChange={handleChange}
58+
/>
59+
))}
60+
</Flex>
61+
);
62+
}, [visible, pickerData, selectedValue, restProps]);
4763

4864
if (displayType === 'modal') {
4965
return (
50-
<Modal visible={visible} onClose={handleClose} animationDuration={150}>
51-
<Flex
52-
height={px(50)}
53-
borderBottomWidth={ONE_PIXEL}
54-
borderBottomColor="border"
55-
backgroundColor="white"
56-
paddingHorizontal="x3"
57-
>
58-
<Flex.Item alignItems="flex-start">
59-
<Pressable activeOpacity={activeOpacity} onPress={handleClose} style={styles.cancel}>
60-
<Text variant="p0" color="primary200">
61-
{cancelText}
62-
</Text>
63-
</Pressable>
64-
</Flex.Item>
65-
<Flex.Item alignItems="center">
66-
<Text variant="p0" color="text">
67-
{title}
68-
</Text>
69-
</Flex.Item>
70-
<Flex.Item alignItems="flex-end">
71-
<Pressable activeOpacity={activeOpacity} onPress={handleOk} style={styles.submit}>
72-
<Text variant="p0" color="primary200">
73-
{okText}
66+
<Modal visible={visible} onClose={handleClose} animationDuration={0}>
67+
{
68+
<Flex
69+
height={px(50)}
70+
borderBottomWidth={ONE_PIXEL}
71+
borderBottomColor="border"
72+
backgroundColor="white"
73+
paddingHorizontal="x3"
74+
>
75+
<Flex.Item alignItems="flex-start">
76+
<Pressable activeOpacity={activeOpacity} onPress={handleClose} style={styles.cancel}>
77+
<Text variant="p0" color="primary200">
78+
{cancelText}
79+
</Text>
80+
</Pressable>
81+
</Flex.Item>
82+
<Flex.Item alignItems="center">
83+
<Text variant="p0" color="text">
84+
{title}
7485
</Text>
75-
</Pressable>
76-
</Flex.Item>
77-
</Flex>
86+
</Flex.Item>
87+
<Flex.Item alignItems="flex-end">
88+
<Pressable activeOpacity={activeOpacity} onPress={handleOk} style={styles.submit}>
89+
<Text variant="p0" color="primary200">
90+
{okText}
91+
</Text>
92+
</Pressable>
93+
</Flex.Item>
94+
</Flex>
95+
}
7896
{PickerComp}
7997
</Modal>
8098
);
8199
}
82100
return PickerComp;
83101
};
84102

85-
export default React.memo(NormalPicker);
103+
export default NormalPicker;
86104

87105
const styles = StyleSheet.create({
88106
cancel: { width: '100%', justifyContent: 'center', alignItems: 'flex-start' },

0 commit comments

Comments
 (0)