前言
最近笔者在公司接到一个需求,大概就是有一个list,list不仅做展示,而且还可以做选中、取消选中,同时支持多选,最后一键保存存储到后端。 这个需求实现很容易,如果按照刚入行那会的思路一定是,直接一把梭,性能优化什么的,不存在的,渲染出来就OK啦~ 但是现在看到这种垃圾实现、垃圾代码真的不能忍,性能太差了,必须得优化下,笔者因此做了以下思考
实现思路
考虑到做的是移动端的项目,而且数据量可能会比较大,为了有较好的性能,我的思路如下:
- 在父组件中用state定义一个map变量,存储选中项。
如果选中的的话就向map中添加一个键值对,如果取消选中的话就将map的这个健值对删除。(判断在渲染是否展示选中与反选的时候,直接判断
map[id]是否存在,所以并没有选择数组存储,因为数组存储还需要遍历它) - 用memo将子组件包裹,从而减少不必要的渲染。(memo的第二个参数是一个方法,用此方法进行)
- 最终在提交的将map的key遍历出来,定义一个数组,提交给后端。
代码实现
公司内部的项目代码涉及到业务,就不贴出来了,在此处我们用简单的demo代码代替。
改造前
const mock_data = [{ id: '1' }, { id: '2' }, { id: '3' }, { id: '4' }];
export default function App() {
const [selectedIds, setSelectedIds] = React.useState<string[]>([]);
const [list, filterList] = React.useState(mock_data);
const onPressItem = React.useCallback(
(id) => {
if (selectedIds.findIndex((item) => item === id) < 0) {
setSelectedIds([...selectedIds, id]);
} else {
setSelectedIds(selectedIds.filter((ele) => ele !== id));
}
},
[selectedIds]
);
return (
<div>
{list.map((ele) => (
<div onClick={() => onPressItem(ele.id)}>
<i>
{selectedIds.findIndex((item) => item === ele.id) < 0 ? '✅' : '❌'}
</i>
<i>{ele.id}</i>
</div>
))}
</div>
);
}
改造后
const mock_data = [{ id: '1' }, { id: '2' }, { id: '3' }, { id: '4' }];
// 抽离每个Item
const Item = React.memo(
function I(props: {
item: { id: string };
map: { [key: string]: string };
onPress: (id: string) => void;
}) {
const { item, map, onPress } = props;
const selected = React.useMemo(() => {
return !!map?.[item.id];
}, [map]);
return (
<div onClick={() => onPress(item.id)}>
<i>{selected ? '✅' : '❌'}</i>
<i>{item.id}</i>
</div>
);
},
(pre, next) => pre.map[pre.item.id] === next.map[next.item.id]
);
// 抽离出List
const List = (props: {
list: any[];
renderItem: (item: any) => React.ReactElement;
}) => {
const { list, renderItem } = props;
return (
<div>
{list.map((ele) => {
return renderItem(ele);
})}
</div>
);
};
export default function App() {
const [map, setMap] = React.useState({});
const [list, filterList] = React.useState(mock_data);
const onPresItem = React.useCallback(
(id) => {
// 第一种方式
// const newMap = JSON.parse(JSON.stringify(map));
// if (newMap[id]) {
// delete newMap[id];
// } else {
// newMap[id] = id;
// }
// setMap(newMap);
// 第二种方式
setMap((pre) => {
if (pre[id]) {
const newResult = { ...pre };
delete newResult[id];
return newResult;
} else {
return { ...pre, [id]: id };
}
});
},
[map]
);
return (
<List
list={list}
renderItem={(item) => (
<Item key={item.id} map={map} item={item} onPress={onPresItem} />
)}
/>
);
}
性能的提升
经过改造,性能果然有很大的提升,尤其是列表中数据多的时候,之前点击会有比较明显的卡顿,但是改造后丝滑得很。具体的数据和视频上传上来比较麻烦,就不进行这一步了。
其实前后主要是对比:
- 用memo包裹后,减少了很多不必要的渲染。
- 使用map存储id后,直接取消了遍历,大大降低了算法复杂度。之前需要遍历数组,而现在直接通过key去访问数据。
残留的疑问
大家可以看到我的改造后的代码,其实在onPresItem中有两种方式,第一种方式的话会造成数据紊乱,而第二种方式并没有出现问题。
咨询了下AI,给出的答案
其实我还是不太懂,引用关系丢失为什么引起数据的紊乱,具体要深入到react的更新机制中吗?