React列表性能优化

250 阅读3分钟

前言

最近笔者在公司接到一个需求,大概就是有一个list,list不仅做展示,而且还可以做选中、取消选中,同时支持多选,最后一键保存存储到后端。 这个需求实现很容易,如果按照刚入行那会的思路一定是,直接一把梭,性能优化什么的,不存在的,渲染出来就OK啦~ 但是现在看到这种垃圾实现、垃圾代码真的不能忍,性能太差了,必须得优化下,笔者因此做了以下思考

实现思路

考虑到做的是移动端的项目,而且数据量可能会比较大,为了有较好的性能,我的思路如下:

  1. 在父组件中用state定义一个map变量,存储选中项。 如果选中的的话就向map中添加一个键值对,如果取消选中的话就将map的这个健值对删除。(判断在渲染是否展示选中与反选的时候,直接判断map[id]是否存在,所以并没有选择数组存储,因为数组存储还需要遍历它)
  2. 用memo将子组件包裹,从而减少不必要的渲染。(memo的第二个参数是一个方法,用此方法进行)
  3. 最终在提交的将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} />
      )}
    />
  );
}

性能的提升

经过改造,性能果然有很大的提升,尤其是列表中数据多的时候,之前点击会有比较明显的卡顿,但是改造后丝滑得很。具体的数据和视频上传上来比较麻烦,就不进行这一步了。

其实前后主要是对比:

  1. 用memo包裹后,减少了很多不必要的渲染
  2. 使用map存储id后,直接取消了遍历,大大降低了算法复杂度。之前需要遍历数组,而现在直接通过key去访问数据。

残留的疑问

大家可以看到我的改造后的代码,其实在onPresItem中有两种方式,第一种方式的话会造成数据紊乱,而第二种方式并没有出现问题。 咨询了下AI,给出的答案

image.png

其实我还是不太懂,引用关系丢失为什么引起数据的紊乱,具体要深入到react的更新机制中吗?