React Native 奇怪的行为来呈现 FlatList
React Native odd behavior to render FlatList
我的目标是一次渲染 20 个项目,而不是渲染 10 个项目,然后在 600 毫秒后重新渲染 20 个项目。
我确保只在获取完成后渲染 FlatList。我依靠 React.useEffect
和 data
依赖项来重新渲染,以模拟效果 load more when scrolling down
。行为是一样的,当向下滚动时,它会重新渲染整个 FlatList 并且每 10 个项目重新渲染一次。
console.log
[Tue Sep 15 2020 08:31:56.797] LOG Album() useEffect()
[Tue Sep 15 2020 08:31:57.510] LOG Album() renderItem() render 1
[Tue Sep 15 2020 08:31:57.651] LOG Album() renderItem() render 2
[Tue Sep 15 2020 08:31:57.758] LOG Album() renderItem() render 3
[Tue Sep 15 2020 08:31:57.851] LOG Album() renderItem() render 4
[Tue Sep 15 2020 08:31:58.700] LOG Album() renderItem() render 5
[Tue Sep 15 2020 08:31:58.175] LOG Album() renderItem() render 6
[Tue Sep 15 2020 08:31:58.272] LOG Album() renderItem() render 7
[Tue Sep 15 2020 08:31:58.375] LOG Album() renderItem() render 8
[Tue Sep 15 2020 08:31:58.525] LOG Album() renderItem() render 9
[Tue Sep 15 2020 08:31:58.654] LOG Album() renderItem() render 10
[Tue Sep 15 2020 08:32:04.758] LOG Album() renderItem() render 1
[Tue Sep 15 2020 08:32:04.940] LOG Album() renderItem() render 2
[Tue Sep 15 2020 08:32:05.106] LOG Album() renderItem() render 3
[Tue Sep 15 2020 08:32:05.240] LOG Album() renderItem() render 4
[Tue Sep 15 2020 08:32:05.420] LOG Album() renderItem() render 5
[Tue Sep 15 2020 08:32:05.530] LOG Album() renderItem() render 6
[Tue Sep 15 2020 08:32:05.608] LOG Album() renderItem() render 7
[Tue Sep 15 2020 08:32:05.737] LOG Album() renderItem() render 8
[Tue Sep 15 2020 08:32:05.885] LOG Album() renderItem() render 9
[Tue Sep 15 2020 08:32:05.986] LOG Album() renderItem() render 10
[Tue Sep 15 2020 08:32:06.310] LOG Album() renderItem() render 11
[Tue Sep 15 2020 08:32:06.920] LOG Album() renderItem() render 12
[Tue Sep 15 2020 08:32:06.148] LOG Album() renderItem() render 13
[Tue Sep 15 2020 08:32:06.201] LOG Album() renderItem() render 14
[Tue Sep 15 2020 08:32:06.280] LOG Album() renderItem() render 15
[Tue Sep 15 2020 08:32:06.340] LOG Album() renderItem() render 16
[Tue Sep 15 2020 08:32:06.414] LOG Album() renderItem() render 17
[Tue Sep 15 2020 08:32:06.457] LOG Album() renderItem() render 18
[Tue Sep 15 2020 08:32:06.521] LOG Album() renderItem() render 19
[Tue Sep 15 2020 08:32:06.589] LOG Album() renderItem() render 20
./src/features/Album/index.js
import React from 'react';
import {
StyleSheet,
ActivityIndicator,
SafeAreaView,
View,
TouchableOpacity,
Text,
FlatList,
RefreshControl,
Animated,
} from 'react-native';
import Swipeable from 'react-native-gesture-handler/Swipeable';
const styles = StyleSheet.create({
item: {
paddingVertical: 20,
paddingHorizontal: 10,
},
separator: {
flex: 1,
height: 1,
backgroundColor: '#e4e4e4',
marginLeft: 10,
},
leftActions: {
backgroundColor: '#388e3c',
justifyContent: 'center',
flex: 1, // continue to swipe.
},
actionText: {
color: '#fff',
fontWeight: '600',
padding: 20,
},
rightActions: {
backgroundColor: '#dd2c00',
justifyContent: 'center',
// flex: 1, // continue to swipe.
alignItems: 'flex-end',
},
});
function LeftActions(progress, dragX) {
// console.log('Album() LeftActions()', progress, dragX)
const scale = dragX.interpolate({
inputRange: [0, 100],
outputRange: [0, 1],
extrapolate: 'clamp',
});
return (
<View style={styles.leftActions}>
<Animated.Text style={[styles.actionText, {transform: [{scale}]}]}>
Add to Cart
</Animated.Text>
</View>
);
}
function RightActions({progress, dragX, onPress}) {
// console.log('Album() LeftActions()', progress, dragX)
const scale = dragX.interpolate({
inputRange: [-100, 0],
outputRange: [1, 0],
extrapolate: 'clamp',
});
return (
<TouchableOpacity onPress={onPress}>
<View style={styles.rightActions}>
<Animated.Text style={[styles.actionText, {transform: [{scale}]}]}>
Delete
</Animated.Text>
</View>
</TouchableOpacity>
);
}
function Item({item, onSwipeFromLeft, onRightPress}) {
// console.log('Album() Item() render')
return (
<Swipeable
renderLeftActions={LeftActions}
onSwipeableLeftOpen={onSwipeFromLeft}
renderRightActions={(progress, dragX) => <RightActions progress={progress} dragX={dragX} onPress={onRightPress} />}
>
<View style={[styles.item]}>
<Text>{`${item.id} ${item.title}`}</Text>
</View>
</Swipeable>
);
}
function Separator() {
return <View style={styles.separator} />;
}
export default function Album() {
const [isLoading, setLoading] = React.useState(true);
const [currentPage, setCurrentPage] = React.useState(1);
// const [itemPerPage, setItemPerPage] = React.useState(20);
const itemPerPage = 20;
const [data, setData] = React.useState([]);
const [error, setError] = React.useState();
React.useEffect(() => {
console.log('Album() useEffect()');
fetch(
`http://jsonplaceholder.typicode.com/albums?_start=${
currentPage * itemPerPage - itemPerPage
}&_limit=${itemPerPage}`,
)
.then((response) => response.json())
.then((json) => setData((initial) => [...initial, ...json]))
.catch((e) => setError(e))
.finally(() => setLoading(false));
}, [currentPage]);
// console.log('Album()', data);
// question: VirtualizedList: You have a large list that is slow to update - make sure your renderItem function renders components that follow React performance best practices like PureComponent, shouldComponentUpdate, etc. {"contentLength": 3179.666748046875, "dt": 627, "prevDt": 658}
const memo = React.useMemo(() => renderItem, [data]);
function renderItem({item}) {
console.log('Album() renderItem() render', item.id);
return (
<Item
item={item}
onSwipeFromLeft={() => alert('swiped from the left')}
onRightPress={() => alert('pressed right')}
/>
);
}
function handleRefresh() {
console.log('Album() handleRefresh()');
setLoading(true);
// simulate fetch
setTimeout(() => {
setLoading(false);
}, 1000);
}
function handleLoadMore() {
// fake limitPage
if (100 / itemPerPage === currentPage) return;
console.log('Album() handleLoadMore()');
setCurrentPage((page) => page + 1);
}
return (
<>
{isLoading ? (
<ActivityIndicator />
) : (
<>
{error && <Text>{error}</Text>}
<Text>Album</Text>
<SafeAreaView>
<FlatList
data={data}
// Is this parameter bad for performance? Put it into perspective, it renders 1,5x more if this parameter is set to half.
// initialNumToRender={itemPerPage / 2}
renderItem={memo}
keyExtractor={(item) =>
'_' + Math.random().toString(36).substr(2, 9) + item.id
}
refreshControl={
<RefreshControl
refreshing={isLoading}
onRefresh={handleRefresh}
/>
}
onEndReachedThreshold={0.1}
onEndReached={handleLoadMore}
ItemSeparatorComponent={Separator}
/>
</SafeAreaView>
</>
)}
</>
);
}
这就是 Flatlist 的工作原理。
Flatlist 建立在 virtualizedList
之上,它只渲染屏幕可见的项目和滚动条,它会自动 remove/unmount 屏幕外的项目,因此提供一种优化的方法来处理巨大的列表。
但在您的情况下,如果它始终为 20,您可以尝试设置 initialNumToRender
并且大多数情况下应该可行。
https://reactnative.dev/docs/flatlist#initialnumtorender
进一步阅读,https://reactnative.dev/docs/optimizing-flatlist-configuration
我的目标是一次渲染 20 个项目,而不是渲染 10 个项目,然后在 600 毫秒后重新渲染 20 个项目。
我确保只在获取完成后渲染 FlatList。我依靠 React.useEffect
和 data
依赖项来重新渲染,以模拟效果 load more when scrolling down
。行为是一样的,当向下滚动时,它会重新渲染整个 FlatList 并且每 10 个项目重新渲染一次。
console.log
[Tue Sep 15 2020 08:31:56.797] LOG Album() useEffect()
[Tue Sep 15 2020 08:31:57.510] LOG Album() renderItem() render 1
[Tue Sep 15 2020 08:31:57.651] LOG Album() renderItem() render 2
[Tue Sep 15 2020 08:31:57.758] LOG Album() renderItem() render 3
[Tue Sep 15 2020 08:31:57.851] LOG Album() renderItem() render 4
[Tue Sep 15 2020 08:31:58.700] LOG Album() renderItem() render 5
[Tue Sep 15 2020 08:31:58.175] LOG Album() renderItem() render 6
[Tue Sep 15 2020 08:31:58.272] LOG Album() renderItem() render 7
[Tue Sep 15 2020 08:31:58.375] LOG Album() renderItem() render 8
[Tue Sep 15 2020 08:31:58.525] LOG Album() renderItem() render 9
[Tue Sep 15 2020 08:31:58.654] LOG Album() renderItem() render 10
[Tue Sep 15 2020 08:32:04.758] LOG Album() renderItem() render 1
[Tue Sep 15 2020 08:32:04.940] LOG Album() renderItem() render 2
[Tue Sep 15 2020 08:32:05.106] LOG Album() renderItem() render 3
[Tue Sep 15 2020 08:32:05.240] LOG Album() renderItem() render 4
[Tue Sep 15 2020 08:32:05.420] LOG Album() renderItem() render 5
[Tue Sep 15 2020 08:32:05.530] LOG Album() renderItem() render 6
[Tue Sep 15 2020 08:32:05.608] LOG Album() renderItem() render 7
[Tue Sep 15 2020 08:32:05.737] LOG Album() renderItem() render 8
[Tue Sep 15 2020 08:32:05.885] LOG Album() renderItem() render 9
[Tue Sep 15 2020 08:32:05.986] LOG Album() renderItem() render 10
[Tue Sep 15 2020 08:32:06.310] LOG Album() renderItem() render 11
[Tue Sep 15 2020 08:32:06.920] LOG Album() renderItem() render 12
[Tue Sep 15 2020 08:32:06.148] LOG Album() renderItem() render 13
[Tue Sep 15 2020 08:32:06.201] LOG Album() renderItem() render 14
[Tue Sep 15 2020 08:32:06.280] LOG Album() renderItem() render 15
[Tue Sep 15 2020 08:32:06.340] LOG Album() renderItem() render 16
[Tue Sep 15 2020 08:32:06.414] LOG Album() renderItem() render 17
[Tue Sep 15 2020 08:32:06.457] LOG Album() renderItem() render 18
[Tue Sep 15 2020 08:32:06.521] LOG Album() renderItem() render 19
[Tue Sep 15 2020 08:32:06.589] LOG Album() renderItem() render 20
./src/features/Album/index.js
import React from 'react';
import {
StyleSheet,
ActivityIndicator,
SafeAreaView,
View,
TouchableOpacity,
Text,
FlatList,
RefreshControl,
Animated,
} from 'react-native';
import Swipeable from 'react-native-gesture-handler/Swipeable';
const styles = StyleSheet.create({
item: {
paddingVertical: 20,
paddingHorizontal: 10,
},
separator: {
flex: 1,
height: 1,
backgroundColor: '#e4e4e4',
marginLeft: 10,
},
leftActions: {
backgroundColor: '#388e3c',
justifyContent: 'center',
flex: 1, // continue to swipe.
},
actionText: {
color: '#fff',
fontWeight: '600',
padding: 20,
},
rightActions: {
backgroundColor: '#dd2c00',
justifyContent: 'center',
// flex: 1, // continue to swipe.
alignItems: 'flex-end',
},
});
function LeftActions(progress, dragX) {
// console.log('Album() LeftActions()', progress, dragX)
const scale = dragX.interpolate({
inputRange: [0, 100],
outputRange: [0, 1],
extrapolate: 'clamp',
});
return (
<View style={styles.leftActions}>
<Animated.Text style={[styles.actionText, {transform: [{scale}]}]}>
Add to Cart
</Animated.Text>
</View>
);
}
function RightActions({progress, dragX, onPress}) {
// console.log('Album() LeftActions()', progress, dragX)
const scale = dragX.interpolate({
inputRange: [-100, 0],
outputRange: [1, 0],
extrapolate: 'clamp',
});
return (
<TouchableOpacity onPress={onPress}>
<View style={styles.rightActions}>
<Animated.Text style={[styles.actionText, {transform: [{scale}]}]}>
Delete
</Animated.Text>
</View>
</TouchableOpacity>
);
}
function Item({item, onSwipeFromLeft, onRightPress}) {
// console.log('Album() Item() render')
return (
<Swipeable
renderLeftActions={LeftActions}
onSwipeableLeftOpen={onSwipeFromLeft}
renderRightActions={(progress, dragX) => <RightActions progress={progress} dragX={dragX} onPress={onRightPress} />}
>
<View style={[styles.item]}>
<Text>{`${item.id} ${item.title}`}</Text>
</View>
</Swipeable>
);
}
function Separator() {
return <View style={styles.separator} />;
}
export default function Album() {
const [isLoading, setLoading] = React.useState(true);
const [currentPage, setCurrentPage] = React.useState(1);
// const [itemPerPage, setItemPerPage] = React.useState(20);
const itemPerPage = 20;
const [data, setData] = React.useState([]);
const [error, setError] = React.useState();
React.useEffect(() => {
console.log('Album() useEffect()');
fetch(
`http://jsonplaceholder.typicode.com/albums?_start=${
currentPage * itemPerPage - itemPerPage
}&_limit=${itemPerPage}`,
)
.then((response) => response.json())
.then((json) => setData((initial) => [...initial, ...json]))
.catch((e) => setError(e))
.finally(() => setLoading(false));
}, [currentPage]);
// console.log('Album()', data);
// question: VirtualizedList: You have a large list that is slow to update - make sure your renderItem function renders components that follow React performance best practices like PureComponent, shouldComponentUpdate, etc. {"contentLength": 3179.666748046875, "dt": 627, "prevDt": 658}
const memo = React.useMemo(() => renderItem, [data]);
function renderItem({item}) {
console.log('Album() renderItem() render', item.id);
return (
<Item
item={item}
onSwipeFromLeft={() => alert('swiped from the left')}
onRightPress={() => alert('pressed right')}
/>
);
}
function handleRefresh() {
console.log('Album() handleRefresh()');
setLoading(true);
// simulate fetch
setTimeout(() => {
setLoading(false);
}, 1000);
}
function handleLoadMore() {
// fake limitPage
if (100 / itemPerPage === currentPage) return;
console.log('Album() handleLoadMore()');
setCurrentPage((page) => page + 1);
}
return (
<>
{isLoading ? (
<ActivityIndicator />
) : (
<>
{error && <Text>{error}</Text>}
<Text>Album</Text>
<SafeAreaView>
<FlatList
data={data}
// Is this parameter bad for performance? Put it into perspective, it renders 1,5x more if this parameter is set to half.
// initialNumToRender={itemPerPage / 2}
renderItem={memo}
keyExtractor={(item) =>
'_' + Math.random().toString(36).substr(2, 9) + item.id
}
refreshControl={
<RefreshControl
refreshing={isLoading}
onRefresh={handleRefresh}
/>
}
onEndReachedThreshold={0.1}
onEndReached={handleLoadMore}
ItemSeparatorComponent={Separator}
/>
</SafeAreaView>
</>
)}
</>
);
}
这就是 Flatlist 的工作原理。
Flatlist 建立在 virtualizedList
之上,它只渲染屏幕可见的项目和滚动条,它会自动 remove/unmount 屏幕外的项目,因此提供一种优化的方法来处理巨大的列表。
但在您的情况下,如果它始终为 20,您可以尝试设置 initialNumToRender
并且大多数情况下应该可行。
https://reactnative.dev/docs/flatlist#initialnumtorender
进一步阅读,https://reactnative.dev/docs/optimizing-flatlist-configuration