- 本文通过两个实际案例,向大家介绍flatlist的机制,优化思路,滚动白屏的原因及优化,全选模型的卡顿优化
FlatList的机制
-
FlatList的原理是实际上是动态创建新的视图,释放旧的视图,而不是重用!!!
-
flatList刷新时会触发所有创建出来的item重绘
-
flatList的renderItem参数中的item和data里面的item不是同一个,并且每次都是新的。
-
如果你要对Item进行memo,那么把item整个传进去就是无效memo。
❌
const renderItem = ({ item }: { item: ItemData }) => <Item item={item} />; -
应该将具体的参数传进去
✅
const renderItem = ({ item }: { item: ItemData }) => <Item id={item.id} title={item.title} />
-
-
renderItem使用useCallback包裹,避免每次渲染都重新创建。但是要注意依赖数组,依赖经常会变的,那也是无效useCallback。
const [checked, setChecked] = useState<Set<number>>(new Set()); //❌ 依赖了checked,经常会修改,无效 const renderItem = useCallback( ({ item }: { item: ItemData }) => ( <View style={styles.row}> ... </View> ), [checked] );
FlatList优化方向
-
renderItem的内容使用官方自带的组件性能最好,并且减少嵌套优化结构,减少item里无用的useEffect,useCallback,useMemo
-
对ItemView进行memo,注意不要直接传整个item
-
防止不相关的state变化时,触发整个flatlist重绘,尽量只影响单个item
-
将
removeClippedSubviews设置为true,把不在屏幕内的视图从原生视图层级结构分离,不进行渲染和绘图遍历。(在iOS上可能有bug,特别是使用动画和绝对定位做复杂的事情,android是默认开的) -
修改
windowSize(默认21),修改为11或者更小,减少滚动时需要刷新的item数量从而提升性能。- 这个是控制flatlist最多创建多少个屏幕的数据,数值越大,创建的item越多
- 这个值越少,在滚动时就更可能白屏,因为滚得多的话,会频繁重新创建和销毁
-
修改
initRenderSize,修改成刚好一个屏幕或者多一两个的item数量。这个是提升首次的速度- 具体数值要根据你的item的高度来估算,可以先对item进行测量
-
实现
getItemSize,这个是让flatlist跳过计算item高度进行layout的过程,可以加速绘制。- 这个就比较复杂了,如果高度是一样的,那就比较方便。能实现最好
FlatList 滚动白屏解析和优化
- 滚动白屏的原因是绘制帧率低于滚动时UI线程的帧率。
- 滚动是原生的的,绘制是在js上的,js绘制比滚动慢,就会白屏
快速滚动白屏
| 快速滚动白屏优化前 | 快速滚动白屏优化后 |
|---|---|
| 白屏情况大幅减少,基本上不会出现,(偶尔还是会有一些)解决方案我们最后再说 |
优化过程
- 原关键代码的问题:
- ❌renderItem是无效useCallback
- ❌renderItem返回的View没有缓存
const renderItem = useCallback(
({ item }: { item: ItemData }) => (
<View style={styles.row}>
...
</View>
),
[checked, toggleCheck]
);
return (
<FlatList
data={DATA}
renderItem={renderItem}
keyExtractor={(item) => String(item.id)}
style={styles.list}
/>
);
- 优化后
- 整个和FlatList相关的使用useMemo缓存,这里也可以用另一种方式:状态提升,我们放到sectionlist里展示
- renderItem返回item抽成Memo后的组件Item
- flatList增加参数
windowSize和initialNumToRender
...
const renderMemoList = useMemo(() => {
const toggleCheck = (id: number) => {
checkedSet.current.has(id)
? checkedSet.current.delete(id)
: checkedSet.current.add(id);
setCheckedSize(checkedSet.current.size);
};
const renderItem = ({ item }: { item: ItemData }) => {
return (
<Item
defaultChecked={checkedSet.current.has(item.id)}
id={item.id}
title={item.title}
onValueChange={toggleCheck}
/>
);
};
return (
<FlatList
data={DATA}
renderItem={renderItem}
keyExtractor={(item) => String(item.id)}
style={styles.list}
windowSize={11}
initialNumToRender={20}
/>
);
}, []);
return (
<View style={{ flex: 1, marginTop: 35 }}>
<Text>Checked count: {checkedSize}</Text>
{renderMemoList}
</View>
);
点击checkbox卡顿
| 点击checkbox优化前 | 点击checkbox优化后 |
|---|---|
| 耗时400ms | 优化后点击: 只需要4毫秒 |
优化过程
- 原来保存已勾选的id,由state变成ref。界面上要显示的选中数量,变成state(checkedSize)
// const [checked, setChecked] = useState<Set<number>>(new Set());
const checkedSet = useRef(new Set());
const [checkedSize, setCheckedSize] = useState(checkedSet.current.size); // 仅用于显示计数
- 将点击checkbox从渲染整个flatlist转变成仅点击的Item重新渲染(checked状态由每个Item去控制)
- 点击后更新checkedSize,因为用了useMemo缓存,更新checkedSize并不会影响到flatlist
const renderMemoList = useMemo(() => {
const toggleCheck = (id: number) => {
checkedSet.current.has(id)
? checkedSet.current.delete(id)
: checkedSet.current.add(id);
setCheckedSize(checkedSet.current.size); // 更新计数
};
const renderItem = ({ item }: { item: ItemData }) => {
return (
<Item
defaultChecked={checkedSet.current.has(item.id)}
id={item.id}
title={item.title}
onValueChange={toggleCheck}
/>
);
};
return (
<FlatList
data={DATA}
renderItem={renderItem}
keyExtractor={(item) => String(item.id)}
style={styles.list}
windowSize={11}
initialNumToRender={20}
/>
);
}, []); // 空依赖 → FlatList永不重渲染
完整代码示例
- 优化前
import { useState, useCallback } from 'react';
import {
View,
Text,
FlatList,
TouchableOpacity,
StyleSheet,
} from 'react-native';
interface ItemData {
id: number;
title: string;
}
const DATA: ItemData[] = Array.from({ length: 1000 }, (_, i) => ({
id: i,
title: `Item ${i}`,
}));
export default function FlatListScrollTest() {
const [checked, setChecked] = useState<Set<number>>(new Set());
const toggleCheck = useCallback((id: number) => {
setChecked((prev) => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
return next;
});
}, []);
const renderItem = useCallback(
({ item }: { item: ItemData }) => (
<View style={styles.row}>
<TouchableOpacity
style={[styles.checkbox, checked.has(item.id) && styles.checked]}
onPress={() => toggleCheck(item.id)}
>
{checked.has(item.id) && <Text style={styles.checkmark}>✓</Text>}
</TouchableOpacity>
<Text style={styles.title} numberOfLines={1}>
{item.title}
</Text>
<TouchableOpacity
style={styles.button}
onPress={() => console.log(`Button ${item.id} pressed`)}
>
<Text style={styles.buttonText}>Click</Text>
</TouchableOpacity>
</View>
),
[checked, toggleCheck]
);
return (
<FlatList
data={DATA}
renderItem={renderItem}
keyExtractor={(item) => String(item.id)}
style={styles.list}
/>
);
}
const styles = StyleSheet.create({
list: {
flex: 1,
},
row: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16,
paddingVertical: 12,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: '#e0e0e0',
},
checkbox: {
width: 24,
height: 24,
borderWidth: 2,
borderColor: '#999',
borderRadius: 4,
justifyContent: 'center',
alignItems: 'center',
marginRight: 12,
},
checked: {
backgroundColor: '#4caf50',
borderColor: '#4caf50',
},
checkmark: {
color: '#fff',
fontSize: 16,
fontWeight: 'bold',
},
title: {
flex: 1,
fontSize: 16,
},
button: {
backgroundColor: '#2196f3',
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 4,
marginLeft: 12,
},
buttonText: {
color: '#fff',
fontSize: 14,
},
});
- 优化后
import { useState, useRef, memo, useMemo } from 'react';
import {
View,
Text,
FlatList,
TouchableOpacity,
StyleSheet,
} from 'react-native';
import { SimpleCheckBox } from './SimpleCheckBox';
interface ItemData {
id: number;
title: string;
}
const DATA: ItemData[] = Array.from({ length: 1000 }, (_, i) => ({
id: i,
title: `Item ${i}`,
}));
const Item = memo(
(props: {
defaultChecked: boolean;
id: number;
title: string;
onValueChange: (id: number, value: boolean) => void;
}) => {
const [checked, setChecked] = useState(props.defaultChecked);
return (
<View style={styles.row}>
<SimpleCheckBox
value={checked}
onValueChange={() => {
setChecked(!checked);
props.onValueChange(props.id, !checked);
}}
/>
<Text style={styles.title} numberOfLines={1}>
{props.title}
</Text>
<TouchableOpacity
style={styles.button}
onPress={() => console.log(`Button ${props.id} pressed`)}
>
<Text style={styles.buttonText}>Click</Text>
</TouchableOpacity>
</View>
);
}
);
export default function FlatListScrollTest() {
// const [checked, setChecked] = useState<Set<number>>(new Set());
// const toggleCheck = useCallback((id: number) => {
// setChecked((prev) => {
// const next = new Set(prev);
// if (next.has(id)) {
// next.delete(id);
// } else {
// next.add(id);
// }
// return next;
// });
// }, []);
// const renderItem = useCallback(
// ({ item }: { item: ItemData }) => (
// <View style={styles.row}>
// <SimpleCheckBox value={checked.has(item.id)} onValueChange={() => toggleCheck(item.id)}/>
// <Text style={styles.title} numberOfLines={1}>
// {item.title}
// </Text>
// <TouchableOpacity
// style={styles.button}
// onPress={() => console.log(`Button ${item.id} pressed`)}
// >
// <Text style={styles.buttonText}>Click</Text>
// </TouchableOpacity>
// </View>
// ),
// [checked, toggleCheck]
// );
const checkedSet = useRef(new Set());
const [checkedSize, setCheckedSize] = useState(checkedSet.current.size);
const renderMemoList = useMemo(() => {
const toggleCheck = (id: number) => {
checkedSet.current.has(id)
? checkedSet.current.delete(id)
: checkedSet.current.add(id);
setCheckedSize(checkedSet.current.size);
};
const renderItem = ({ item }: { item: ItemData }) => {
return (
<Item
defaultChecked={checkedSet.current.has(item.id)}
id={item.id}
title={item.title}
onValueChange={toggleCheck}
/>
);
};
return (
<FlatList
data={DATA}
renderItem={renderItem}
keyExtractor={(item) => String(item.id)}
style={styles.list}
windowSize={11}
initialNumToRender={20}
/>
);
}, []);
return (
<View style={{ flex: 1, marginTop: 35 }}>
<Text>Checked count: {checkedSize}</Text>
{renderMemoList}
</View>
);
}
const styles = StyleSheet.create({
list: {
flex: 1,
},
row: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16,
paddingVertical: 12,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: '#e0e0e0',
},
checkbox: {
width: 24,
height: 24,
borderWidth: 2,
borderColor: '#999',
borderRadius: 4,
justifyContent: 'center',
alignItems: 'center',
marginRight: 12,
},
checked: {
backgroundColor: '#4caf50',
borderColor: '#4caf50',
},
checkmark: {
color: '#fff',
fontSize: 16,
fontWeight: 'bold',
},
title: {
flex: 1,
fontSize: 16,
},
button: {
backgroundColor: '#2196f3',
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 4,
marginLeft: 12,
},
buttonText: {
color: '#fff',
fontSize: 14,
},
});
SectionList 全选模型卡顿的解析和优化
点击checkbox卡顿
| 优化前 | 优化后 |
|---|---|
| 大概300ms | 只需要10ms了很流畅 |
优化过程
- 这次我们使用更改页面结构,用状态提升的方式,来让sectionList不受其他的state影响。就不需要useMemo了
- 其他也是和flatlist差不多,check状态放到item里,保存已勾选的state换成ref,增加windowsize
// 优化前
...
return (
<View style={styles.container}>
{/* 顶部:全选 */}
<View style={styles.topBar}>
<SimpleCheckBox value={allSelected} onValueChange={toggleSelectAll} />
<Text style={styles.selectAllText}>
全选 ({checked.size}/{ALL_IDS.length})
</Text>
</View>
{/* 中间:SectionList */}
<SectionList
sections={sections}
renderItem={renderItem}
renderSectionHeader={renderSectionHeader}
keyExtractor={(item) => String(item.id)}
stickySectionHeadersEnabled
style={styles.list}
/>
{/* 底部:按钮 */}
<View style={styles.bottomBar}>
<Button
title={`已选 ${checked.size} 项,点击确认`}
onPress={() => console.log('Selected IDs:', [...checked])}
/>
</View>
</View>
);
// 优化后
return (
<RootLayout
toggleSelectAll={toggleSelectAll}
onPress={() => {
console.log('已选: ', checkedSet.current);
}}
>
<SectionList
sections={SECTIONS}
renderItem={renderItem}
renderSectionHeader={renderSectionHeader}
keyExtractor={(item) => String(item.id)}
stickySectionHeadersEnabled
style={styles.list}
windowSize={5}
/>
</RootLayout>
);
全选联动
- 但是我们发现这样就没有联动了,比如整个Section 0 都被勾选上,但是Section Head并没有被勾选,同样点击Section 0 也不会对子Item有影响
| 优化前 | 优化后 |
|---|---|
| 勾选一个SectionHead的耗时和已经有多少个item要被渲染有关,全部大概要30ms |
优化过程
- 解决思路:想办法通知相应的view更新视图,不更新所有。这里我们用事件监听的方式。
- 全选按钮、sectionHead和sectionItem里都加上事件监听
// 事件定义
interface EventPrams {
headId: number; // -1表示全选,其他表示sectionId
checkedItemIds: Set<number>; // 当前所有选中的item id ,全选、head和item都可以根据这个来判断自己是否checked
}
// 最顶部的全选按钮和文案
useEffect(() => {
const subscription = DeviceEventEmitter.addListener(
EVENT_NAME,
(data: EventPrams) => {
// 如果size没变是不会渲染的
setCheckedSize(data.checkedItemIds.size);
}
);
return subscription.remove;
}, []);
// head
useEffect(() => {
const subscription = DeviceEventEmitter.addListener(
EVENT_NAME,
(data: EventPrams) => {
if (data.headId !== props.headerId && data.headId !== -1) {
return;
}
const sectionData = SECTIONS.find((s) => s.id === props.headerId);
const isAllChecked = sectionData?.data.every((item) =>
data.checkedItemIds.has(item.id)
);
setAllChecked(!!isAllChecked);
}
);
return subscription.remove;
}, [props.headerId]);
// item
useEffect(() => {
const subscription = DeviceEventEmitter.addListener(
EVENT_NAME,
(data: EventPrams) => {
// -1是全部更新,平时过滤,只看自己的
if (data.headId !== props.headerId && data.headId !== -1) {
return;
}
setChecked(data.checkedItemIds.has(props.id));
}
);
return subscription.remove;
}, [props.headerId, props.id]);
- checkBox的点击,都会发送相应事件
// 点击全选
const toggleSelectAll = () => {
if (checkedSet.current.size === ALL_IDS.length) {
checkedSet.current.clear();
} else {
checkedSet.current = new Set(ALL_IDS);
}
emitEvent({ headId: -1, checkedItemIds: checkedSet.current });
};
// 点击sectionHead
const toggleSection = (
headerId: number,
isChecked: boolean,
isNeedEmitEvent = true
) => {
const sectionData = SECTIONS.find((s) => s.id === headerId);
if (isChecked) {
checkedHeadSet.current.add(headerId);
sectionData &&
sectionData.data.forEach((item) =>
toggleItem(item.id, headerId, true, false)
);
} else {
checkedHeadSet.current.delete(headerId);
sectionData &&
sectionData.data.forEach((item) =>
toggleItem(item.id, headerId, false, false)
);
}
// 全选要把所有子item都选中,非全选要把所有子Item都取消选中
if (isNeedEmitEvent) {
emitEvent({ headId: headerId, checkedItemIds: checkedSet.current });
}
};
// 点击SectionItem
const toggleItem = (
id: number,
headerId: number,
isChecked: boolean,
isNeedEmitEvent = true
) => {
if (isChecked) {
checkedSet.current.add(id);
} else {
checkedSet.current.delete(id);
checkedHeadSet.current.delete(headerId);
}
if (isNeedEmitEvent) {
emitEvent({ headId: headerId, checkedItemIds: checkedSet.current });
}
};
完整代码
- 优化前
import { useCallback, useMemo, useState } from 'react';
import { View, Text, SectionList, StyleSheet, Button } from 'react-native';
import { SimpleCheckBox } from './SimpleCheckBox';
interface ItemData {
id: number;
title: string;
}
interface SectionData {
title: string;
data: ItemData[];
}
const SECTIONS: SectionData[] = Array.from({ length: 10 }, (_, si) => ({
title: `Section ${si}`,
data: Array.from({ length: 100 }, (_, i) => ({
id: si * 100 + i,
title: `Item ${si * 100 + i}`,
})),
}));
const ALL_IDS = SECTIONS.flatMap((s) => s.data.map((item) => item.id));
export default function SectionListSelectAllTest() {
const [checked, setChecked] = useState<Set<number>>(new Set());
const allSelected = checked.size === ALL_IDS.length;
const toggleSelectAll = useCallback(() => {
setChecked((prev) =>
prev.size === ALL_IDS.length ? new Set() : new Set(ALL_IDS)
);
}, []);
const toggleItem = useCallback((id: number) => {
setChecked((prev) => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
return next;
});
}, []);
const toggleSection = useCallback((sectionIds: number[]) => {
setChecked((prev) => {
const next = new Set(prev);
const allInSection = sectionIds.every((id) => next.has(id));
if (allInSection) {
sectionIds.forEach((id) => next.delete(id));
} else {
sectionIds.forEach((id) => next.add(id));
}
return next;
});
}, []);
const renderItem = useCallback(
({ item }: { item: ItemData }) => (
<View style={styles.row}>
<SimpleCheckBox
value={checked.has(item.id)}
onValueChange={() => toggleItem(item.id)}
/>
<Text style={styles.title} numberOfLines={1}>
{item.title}
</Text>
</View>
),
[checked, toggleItem]
);
const renderSectionHeader = useCallback(
({ section }: { section: SectionData }) => {
const sectionIds = section.data.map((item) => item.id);
const allChecked = sectionIds.every((id) => checked.has(id));
return (
<View style={styles.sectionHeader}>
<SimpleCheckBox
value={allChecked}
onValueChange={() => toggleSection(sectionIds)}
/>
<Text style={styles.sectionTitle}>{section.title}</Text>
</View>
);
},
[checked, toggleSection]
);
const sections = useMemo(() => SECTIONS, []);
return (
<View style={styles.container}>
{/* 顶部:全选 */}
<View style={styles.topBar}>
<SimpleCheckBox value={allSelected} onValueChange={toggleSelectAll} />
<Text style={styles.selectAllText}>
全选 ({checked.size}/{ALL_IDS.length})
</Text>
</View>
{/* 中间:SectionList */}
<SectionList
sections={sections}
renderItem={renderItem}
renderSectionHeader={renderSectionHeader}
keyExtractor={(item) => String(item.id)}
stickySectionHeadersEnabled
style={styles.list}
/>
{/* 底部:按钮 */}
<View style={styles.bottomBar}>
<Button
title={`已选 ${checked.size} 项,点击确认`}
onPress={() => console.log('Selected IDs:', [...checked])}
/>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
topBar: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16,
paddingVertical: 12,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: '#e0e0e0',
backgroundColor: '#f5f5f5',
},
selectAllText: {
fontSize: 16,
fontWeight: '600',
marginLeft: 8,
},
list: {
flex: 1,
},
sectionHeader: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#e8e8e8',
paddingHorizontal: 16,
paddingVertical: 8,
},
sectionTitle: {
fontSize: 14,
fontWeight: '700',
color: '#555',
marginLeft: 8,
},
row: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16,
paddingVertical: 10,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: '#eee',
},
title: {
flex: 1,
fontSize: 15,
marginLeft: 8,
},
bottomBar: {
padding: 16,
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: '#e0e0e0',
backgroundColor: '#f5f5f5',
},
});
- 优化后
import { memo, type ReactNode, useEffect, useRef, useState } from 'react';
import {
View,
Text,
SectionList,
StyleSheet,
Button,
DeviceEventEmitter,
} from 'react-native';
import { SimpleCheckBox } from './SimpleCheckBox';
interface ItemData {
id: number;
headId: number;
title: string;
}
interface SectionData {
id: number;
title: string;
data: ItemData[];
}
const SECTIONS: SectionData[] = Array.from({ length: 100 }, (_, si) => ({
id: si,
title: `Section ${si}`,
data: Array.from({ length: 10 }, (_, i) => ({
id: si * 10 + i,
headId: si,
title: `Item ${si * 10 + i}`,
})),
}));
const ALL_IDS = SECTIONS.flatMap((s) => s.data.map((item) => item.id));
const EVENT_NAME = 'SectionDataChangeEvent';
interface EventPrams {
headId: number;
checkedItemIds: Set<number>;
}
const Item = memo(
(props: {
defaultChecked: boolean;
id: number;
headerId: number;
title: string;
onValueChange: (id: number, headerId: number, value: boolean) => void;
}) => {
const [checked, setChecked] = useState(props.defaultChecked);
useEffect(() => {
const subscription = DeviceEventEmitter.addListener(
EVENT_NAME,
(data: EventPrams) => {
if (data.headId !== props.headerId && data.headId !== -1) {
return;
}
setChecked(data.checkedItemIds.has(props.id));
}
);
return subscription.remove;
}, [props.headerId, props.id]);
return (
<View style={styles.row}>
<SimpleCheckBox
value={checked}
onValueChange={() => {
setChecked(!checked);
props.onValueChange(props.id, props.headerId, !checked);
}}
/>
<Text style={styles.title} numberOfLines={1}>
{props.title}
</Text>
</View>
);
}
);
const HeadItem = memo(
(props: {
defaultChecked: boolean;
headerId: number;
title: string;
onValueChange: (headerId: number, value: boolean) => void;
}) => {
const [allChecked, setAllChecked] = useState(props.defaultChecked);
useEffect(() => {
const subscription = DeviceEventEmitter.addListener(
EVENT_NAME,
(data: EventPrams) => {
if (data.headId !== props.headerId && data.headId !== -1) {
return;
}
const sectionData = SECTIONS.find((s) => s.id === props.headerId);
const isAllChecked = sectionData?.data.every((item) =>
data.checkedItemIds.has(item.id)
);
setAllChecked(!!isAllChecked);
}
);
return subscription.remove;
}, [props.headerId]);
return (
<View style={styles.sectionHeader}>
<SimpleCheckBox
value={allChecked}
onValueChange={() => {
props.onValueChange(props.headerId, !allChecked);
setAllChecked(!allChecked);
}}
/>
<Text style={styles.sectionTitle}>{props.title}</Text>
</View>
);
}
);
const RootLayout = (props: {
toggleSelectAll: (isSelectAll: boolean) => void;
onPress: () => void;
children: ReactNode;
}) => {
const [checkedSize, setCheckedSize] = useState(0);
const allSelected = checkedSize === ALL_IDS.length;
useEffect(() => {
const subscription = DeviceEventEmitter.addListener(
EVENT_NAME,
(data: EventPrams) => {
setCheckedSize(data.checkedItemIds.size);
}
);
return subscription.remove;
}, []);
return (
<View style={styles.container}>
{/* 顶部:全选 */}
<View style={styles.topBar}>
<SimpleCheckBox
value={allSelected}
onValueChange={() => {
setCheckedSize(allSelected ? 0 : ALL_IDS.length);
props.toggleSelectAll(!allSelected);
}}
/>
<Text style={styles.selectAllText}>
全选 ({checkedSize}/{ALL_IDS.length})
</Text>
</View>
{/* 中间:SectionList */}
{props.children}
{/* 底部:按钮 */}
<View style={styles.bottomBar}>
<Button
title={`已选 ${checkedSize} 项,点击确认`}
onPress={props.onPress}
/>
</View>
</View>
);
};
export default function SectionListSelectAllTest() {
const checkedSet = useRef<Set<number>>(new Set());
const checkedHeadSet = useRef<Set<number>>(new Set());
const emitEvent = (data: { headId: number; checkedItemIds: Set<number> }) => {
DeviceEventEmitter.emit(EVENT_NAME, data);
};
const toggleSelectAll = () => {
if (checkedSet.current.size === ALL_IDS.length) {
checkedSet.current.clear();
} else {
checkedSet.current = new Set(ALL_IDS);
}
// SECTIONS.forEach(section => toggleSection(section.id, isSelectAll,false));
// SECTIONS.forEach(section => emitEvent({headId:section.id, checkedItemIds: checkedSet.current}));
emitEvent({ headId: -1, checkedItemIds: checkedSet.current });
};
const toggleItem = (
id: number,
headerId: number,
isChecked: boolean,
isNeedEmitEvent = true
) => {
if (isChecked) {
checkedSet.current.add(id);
} else {
checkedSet.current.delete(id);
checkedHeadSet.current.delete(headerId);
}
if (isNeedEmitEvent) {
emitEvent({ headId: headerId, checkedItemIds: checkedSet.current });
}
};
const toggleSection = (
headerId: number,
isChecked: boolean,
isNeedEmitEvent = true
) => {
const sectionData = SECTIONS.find((s) => s.id === headerId);
if (isChecked) {
checkedHeadSet.current.add(headerId);
sectionData &&
sectionData.data.forEach((item) =>
toggleItem(item.id, headerId, true, false)
);
} else {
checkedHeadSet.current.delete(headerId);
sectionData &&
sectionData.data.forEach((item) =>
toggleItem(item.id, headerId, false, false)
);
}
// 全选要把所有子item都选中,非全选要把所有子Item都取消选中
if (isNeedEmitEvent) {
emitEvent({ headId: headerId, checkedItemIds: checkedSet.current });
}
};
const renderItem = ({ item }: { item: ItemData }) => (
<Item
defaultChecked={checkedSet.current.has(item.id)}
id={item.id}
headerId={item.headId}
title={item.title}
onValueChange={toggleItem}
/>
);
const renderSectionHeader = ({ section }: { section: SectionData }) => (
<HeadItem
defaultChecked={checkedHeadSet.current.has(section.id)}
headerId={section.id}
title={section.title}
onValueChange={toggleSection}
/>
);
return (
<RootLayout
toggleSelectAll={toggleSelectAll}
onPress={() => {
console.log('已选: ', checkedSet.current);
}}
>
<SectionList
sections={SECTIONS}
renderItem={renderItem}
renderSectionHeader={renderSectionHeader}
keyExtractor={(item) => String(item.id)}
stickySectionHeadersEnabled
style={styles.list}
windowSize={5}
/>
</RootLayout>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
marginTop: 35,
},
topBar: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16,
paddingVertical: 12,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: '#e0e0e0',
backgroundColor: '#f5f5f5',
},
selectAllText: {
fontSize: 16,
fontWeight: '600',
marginLeft: 8,
},
list: {
flex: 1,
},
sectionHeader: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#e8e8e8',
paddingHorizontal: 16,
paddingVertical: 8,
},
sectionTitle: {
fontSize: 14,
fontWeight: '700',
color: '#555',
marginLeft: 8,
},
row: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16,
paddingVertical: 10,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: '#eee',
},
title: {
flex: 1,
fontSize: 15,
marginLeft: 8,
},
bottomBar: {
padding: 16,
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: '#e0e0e0',
backgroundColor: '#f5f5f5',
},
});
使用FlashList代替FlatList
如果上面的优化还是不能满足你的要求,那么推荐你使用@shopify/flash-list这个库。它是纯js实现的高性能list库。
-
❌依赖的recyclerview库,这个库的使用就比较啰嗦了,和flatlist完全不一致。
-
✅flashlist的接口和flatlist是一致的,非常方便切换
它的核心思想和 Android的RecyclerView 一样:只渲染屏幕可见区域 + 缓冲区的 item,回收离开视口的 item 复用。
- 向下滚动时,将离开屏幕的item,移动到屏幕下方使用
- 向上滚动时,将离开屏幕的item,移动到屏幕上方使用
性能高的原因:它控制了创建的item数量,控制在了极小的数量,并且都是复用,不会重新创建
不过它没有封装类似SectionList和SectionGrid。我对此进行了封装,接口和数据结构与原版基本一致,感兴趣的可以看看。
- FlashSectionList
export interface FlashSection<T> {
data: T[];
}
export type FlashSectionData<T> = {section: FlashSection<T>} | T;
export type FlashSectionListProps<T, S> = Omit<
FlashListProps<FlashSectionData<T>>,
'data' | 'renderItem'
> & {
sections: S[];
headHeight: number;
itemHeight: number;
renderItem: (itemData: {item: T; index: number}) => React.ReactElement | null;
renderSectionHead?: (headData: {section: S}) => React.ReactElement | null;
stickySectionHeadersEnabled?: boolean;
};
const getItemType = item => item.type;
export const FlashSectionList = <T, S extends FlashSection<T>>(
props: FlashSectionListProps<T, S>,
) => {
const {
sections,
itemHeight,
headHeight,
stickySectionHeadersEnabled,
renderItem,
renderSectionHead,
...flashListProps
} = props;
const state = useMemo(() => {
let stickyHeaderArray: number[] = [];
let headIndex = 0;
let newData = sections
.map(item => {
const list = [{section: item, type: 'head'}, ...item.data];
stickyHeaderArray = [...stickyHeaderArray, headIndex];
headIndex += list.length;
return list;
})
.flat();
return {
stickyHeaderIndices: stickyHeaderArray,
data: newData,
};
}, [sections]);
const renderSection = ({item, index}) => {
if (item.type === 'head') {
return renderSectionHead ? renderSectionHead(item) : null;
}
// 原版sectionList的 index是在子列表中的index
let itemIndex = index;
for (let i = state.stickyHeaderIndices.length - 1; i >= 0; i--) {
if (itemIndex > state.stickyHeaderIndices[i]) {
itemIndex = itemIndex - state.stickyHeaderIndices[i] - 1;
break;
}
}
return renderItem({item, index: itemIndex});
};
const estimatedItemSize = renderSectionHead
? (itemHeight + headHeight) / 2
: itemHeight;
return (
<FlashList
{...flashListProps}
data={state.data}
renderItem={renderSection}
stickyHeaderIndices={
stickySectionHeadersEnabled ? state.stickyHeaderIndices : undefined
}
getItemType={getItemType}
estimatedItemSize={estimatedItemSize}
/>
);
};
- FlashSectionGrid
interface FlashSectionGridProps<T, S> extends FlashSectionListProps<T, S> {
numberOfCell: number; // 一行多少个item
}
export const FlashSectionGrid = <T, S extends FlashSection<T>>(
props: FlashSectionGridProps<T, S>,
) => {
const gridData = useMemo(() => {
// 根据 numberOfCell 分割出里面的子数据
return props.sections.map(section => {
if (props.numberOfCell <= 0) {
return section;
}
let newDatas: T[][] = [];
for (let i = 0; i < section.data.length; i += props.numberOfCell) {
newDatas = newDatas.concat([
section.data.slice(i, i + props.numberOfCell),
]);
}
return {...section, data: newDatas};
});
}, [props.sections, props.numberOfCell]);
const cellItemArray = new Array(props.numberOfCell).fill(0);
const renderPerItem = ({item, index}) => {
return (
<View style={styles.gridCellContainer}>
{/*防止 item的项不够时,均分的item不够*/}
{cellItemArray.map((_, dataIndex) => {
return (
<View style={styles.cellContainer} key={dataIndex}>
{dataIndex >= item.length
? undefined
: props.renderItem?.({item: item[dataIndex], index: index})}
</View>
);
})}
</View>
);
};
return (
<FlashSectionList
{...props}
sections={gridData}
renderItem={renderPerItem}
/>
);
};
const styles = StyleSheet.create({
gridCellContainer: {
flexDirection: 'row',
},
cellContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'row',
},
});