ReactNative总结系列四 --- FlatList白屏卡顿优化

0 阅读9分钟
  • 本文通过两个实际案例,向大家介绍flatlist的机制,优化思路,滚动白屏的原因及优化,全选模型的卡顿优化

FlatList的机制

  1. FlatList的原理是实际上是动态创建新的视图,释放旧的视图,而不是重用!!!

  2. flatList刷新时会触发所有创建出来的item重绘

  3. flatList的renderItem参数中的item和data里面的item不是同一个,并且每次都是新的。

    1. 如果你要对Item进行memo,那么把item整个传进去就是无效memo。

      const renderItem = ({ item }: { item: ItemData }) => <Item item={item} />;

    2. 应该将具体的参数传进去

      const renderItem = ({ item }: { item: ItemData }) => <Item id={item.id} title={item.title} />

  4. 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优化方向

  1. renderItem的内容使用官方自带的组件性能最好,并且减少嵌套优化结构,减少item里无用的useEffect,useCallback,useMemo

  2. 对ItemView进行memo,注意不要直接传整个item

  3. 防止不相关的state变化时,触发整个flatlist重绘,尽量只影响单个item

  4. removeClippedSubviews 设置为true,把不在屏幕内的视图从原生视图层级结构分离,不进行渲染和绘图遍历。(在iOS上可能有bug,特别是使用动画和绝对定位做复杂的事情,android是默认开的)

  5. 修改windowSize(默认21),修改为11或者更小,减少滚动时需要刷新的item数量从而提升性能。

    • 这个是控制flatlist最多创建多少个屏幕的数据,数值越大,创建的item越多
    • 这个值越少,在滚动时就更可能白屏,因为滚得多的话,会频繁重新创建和销毁
  6. 修改initRenderSize,修改成刚好一个屏幕或者多一两个的item数量。这个是提升首次的速度

    • 具体数值要根据你的item的高度来估算,可以先对item进行测量
  7. 实现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增加参数windowSizeinitialNumToRender
...
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毫秒

优化过程

  1. 原来保存已勾选的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); // 仅用于显示计数

  1. 将点击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 如果只要1个大概是10ms

优化过程

  • 解决思路:想办法通知相应的view更新视图,不更新所有。这里我们用事件监听的方式。
  1. 全选按钮、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]);
  1. 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',
  },
});