React Native 复杂列表开发与性能优化全攻略(现在看为时不晚!)

937 阅读17分钟

​ ​微信公众号:小武码码码

        大家好,上一篇分享了 Flutter复杂列表开发与性能优化全攻略。那接下来这一篇,我想和大家分享一下在 React Native 中开发复杂列表的经验和心得。作为移动开发中最常见的 UI 组件之一,列表几乎出现在每一个应用中。然而,当列表变得复杂起来,包含各种不同的样式和交互时,如何进行高效的开发和优化,就成了一个值得深入探讨的问题。

在本文中,我将从以下几个方面,与大家深入探讨 React Native 复杂列表的开发和优化策略:

  1. React Native 中常见的复杂列表样式及其应用场景
  2. React Native 复杂列表的几种主要开发方式及其优缺点
  3. React Native 复杂列表的高度自适应优化方案及其实现细节
  4. React Native 复杂列表的性能优化策略及其具体实践
  5. React Native 列表与原生列表的差异及其优劣势比较

让我们开始这一场复杂列表开发和优化之旅吧!

一、React Native 中常见的复杂列表样式及其应用场景

在 App 开发中,我们经常会遇到各种复杂的列表需求。这些复杂列表在样式和功能上往往有其特殊性,需要针对性地进行开发和优化。下面,就让我们一起来看看 React Native 中几种常见的复杂列表样式:

  1. 聊天列表。聊天列表是社交类应用中最常见的列表样式之一,主要用于展示用户之间的对话内容。聊天列表具有如下特点:

    • 列表项可以是文本、图片、语音等多种类型的消息。
    • 列表项根据消息的发送者和时间戳,分左右两种不同的排列样式。
    • 列表项可以包含用户昵称、头像等额外信息。
    • 列表往往可以下拉刷新,上拉加载更多消息。
    • 列表需要支持消息发送状态(发送中/发送成功/发送失败)的显示。
  2. Feed 流列表。Feed 流列表是资讯类和社交类应用中最常见的列表样式之一,主要用于向用户展示持续更新的内容。Feed 流列表具有如下特点:

    • 列表项的内容形式多样,可以包含文字、图片、视频、链接等。
    • 列表项的布局千变万化,既有上下滑动,也有左右滑动。
    • 列表项往往可以进行点赞、评论、转发等交互操作。
    • 列表往往可以下拉刷新,上拉加载更多内容。
    • 列表项可以根据内容类型或者互动数据,显示不同的样式。
  3. 电商商品列表。电商商品列表在电商类应用中非常常见,主要用于展示商品的基本信息和销售数据。电商商品列表具有如下特点:

    • 列表项包含商品的图片、名称、价格、评分等关键信息。
    • 列表提供多种排序方式,如按价格、评分、销量等。
    • 列表支持按品牌、型号等条件进行筛选过滤。
    • 列表支持网格和列表两种视图切换。
    • 列表项点击后可以跳转到商品详情页面。
  4. 复杂表格列表。复杂表格列表主要用于展示结构化的数据,如统计报表、订单明细等。复杂表格列表具有如下特点:

    • 表格包含多列,每一列都有其特定的数据类型和格式。
    • 表格需要对列进行自适应,以便在不同尺寸屏幕上都能完整展示。
    • 表格需要固定表头,以便在纵向滚动时仍然能够看到每一列的名称。
    • 表格需要支持横向滚动,以便展示所有的列。
    • 表格往往需要分页展示,并提供跳转控制。

可以看到,复杂列表的样式和需求千差万别,这就要求我们在开发时,既要保证列表的功能完整性,又要兼顾列表的展示效果和性能体验。那么,在 React Native 中,我们到底应该如何实现这些复杂列表呢?接下来,我们就一起来看看。

二、React Native 复杂列表的几种主要开发方式及其优缺点

React Native 为我们提供了多种开发列表的方式,每一种方式都有其独特的优势和局限。下面,我就来介绍几种主要的复杂列表开发方式:

  1. FlatList 组件FlatList 是 React Native 中最基本的列表组件,用于高效地渲染长列表数据。使用 FlatList,我们只需指定列表数据和渲染函数,即可快速生成可滚动的列表视图。
<FlatList
  data={data}
  renderItem={({item}) => <Item title={item.title} />}
  keyExtractor={item => item.id}
/>

FlatList 的优点在于:

  • 开箱即用,使用简单。
  • 支持下拉刷新、上拉加载等常见交互。
  • 支持 onEndReached 等事件回调。
  • 支持 ListHeaderComponentListFooterComponent 等自定义组件。

然而,FlatList 也存在一些局限性:

  • 不支持 section 分组和 sticky header。
  • 对于复杂的列表项布局,需要自行控制高度和复用。
  • 对于异构列表数据,需要自行处理数据源和渲染逻辑。
  1. SectionList 组件SectionList 是 React Native 中用于渲染分组列表的组件,可以将列表数据按照一定的逻辑分成若干个 section,每个 section 包含一个 header 和若干个 item。
<SectionList
  sections={[
    {title: 'Title1', data: [...]},
    {title: 'Title2', data: [...]},
    {title: 'Title3', data: [...]},
  ]}
  renderItem={({item}) => <Item title={item.title} />}
  renderSectionHeader={({section}) => <Header title={section.title} />}
    keyExtractor={(item, index) => item + index}
/>

SectionList 的优点在于:

  • 支持 section 分组和 sticky header。
  • 对于需要分组展示的列表数据,逻辑更加清晰。
  • 可以对不同 section 的 header 和 item 分别定制样式和逻辑。

然而,SectionList 也存在一些局限性:

  • 只支持纵向滚动,不支持横向滚动。
  • 对于 section 内部的列表项,复用逻辑需要自行控制。
  • 在 section 数量较多时,sticky header 的定位和样式需要细心调试。
  1. ScrollView + 自定义布局。对于一些对样式和交互有特殊需求的列表,我们还可以直接使用 ScrollView 组件,结合自定义的布局来实现。
<ScrollView>
  <View style={styles.item}>
    <Text>{item1.title}</Text>
  </View>
  <View style={styles.item}>
    <Text>{item2.title}</Text>
    <Image source={item2.image} />
  </View>
  <CustomItem data={item3} />
  ...
</ScrollView>

ScrollView + 自定义布局的优点在于:

  • 可以完全自由地控制列表的展示样式和交互逻辑。
  • 可以实现一些非常规的列表布局,如瀑布流、双向滚动等。
  • 可以方便地引入各种自定义组件,提高列表的灵活性。

然而,ScrollView + 自定义布局也存在一些局限性:

  • 需要自行处理列表项的渲染和复用,容易引入性能问题。
  • 需要自行实现下拉刷新、上拉加载等交互逻辑。
  • 对于超长列表,需要考虑分页加载等优化策略。
  1. VirtualizedList 组件VirtualizedList 是 React Native 中最复杂、最灵活的列表组件,它不仅支持常见的列表功能,还提供了一整套完整的列表优化方案。
<VirtualizedList
  data={data}
  renderItem={({item}) => <Item title={item.title} />}
  getItem={(data, index) => data[index]}
  getItemCount={data => data.length}
  keyExtractor={(item, index) => item.id}
  getItemLayout={(data, index) => (
    {length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index}
  )}
/>

VirtualizedList 的优点在于:

  • 支持大数据量的列表渲染,通过懒渲染和回收机制,保证列表的流畅度。
  • 支持列表项高度的预估和缓存,避免了滚动过程中的大量计算。
  • 支持列表项的叠加和吸顶效果,提供了更加丰富的展示形式。

然而,VirtualizedList 也存在一些局限性:

  • 接口和参数较多,上手成本较高。
  • 需要准确地估算列表项高度,否则可能出现白屏等问题。
  • 在某些场景下,需要自行管理列表项的渲染和回收。

以上就是 React Native 中常见的几种复杂列表开发方式,在实际项目中,我们需要根据列表的具体需求和场景,选择合适的方式来实现。当然,无论选择哪种方式,性能始终是需要关注的重点。那么,对于复杂列表的性能优化,我们又该从哪些方面入手呢?让我们继续往下看。

三、React Native 复杂列表的高度自适应优化方案及其实现细节

在 React Native 中,列表的性能很大程度上取决于列表项的高度计算和布局。如果列表项的高度不固定、不可预估,就可能导致滚动过程中的大量重排和重绘,从而影响列表的流畅度。因此,对于复杂列表的性能优化,首要任务就是要实现列表项高度的自适应。

那么,在 React Native 中,我们如何实现列表项高度的自适应呢?下面,我就来介绍几种常见的方案:

  1. 利用 Flex 布局实现列表项高度自适应。对于一些简单的列表项,我们可以利用 Flex 布局来实现高度自适应。具体来说,就是将列表项的根节点设置为 flex: 1,然后在内部使用 flexDirectionjustifyContentalignItems 等属性来控制子节点的排列方式。
<View style={{flex: 1, flexDirection: 'row', alignItems: 'center'}}>
  <Image style={{width: 50, height: 50}} source={item.avatar} />
  <View style={{flex: 1, marginLeft: 10}}>
    <Text style={{fontSize: 18}}>{item.name}</Text>
    <Text style={{fontSize: 14, color: '#999'}}>{item.subtitle}</Text>
  </View>
</View>

在上面的代码中,我们将列表项分为左右两部分,左边是固定尺寸的头像,右边是自适应的文本。通过将右边的容器设置为 flex: 1,就可以让文本区域自动填充剩余空间,从而实现了列表项整体高度的自适应。

这种方式的优点是简单直观,适用于布局比较规则的列表项。然而,它也存在一些局限性:

  • 对于布局不规则、嵌套层级较深的列表项,Flex 布局可能无法完全实现自适应。
  • 当列表项内部出现异步加载的内容(如网络图片)时,容器的高度可能会发生变化,导致布局错乱。
  1. 使用 onLayout 方法获取列表项的实际高度。对于一些高度不固定,依赖于内容的列表项,我们可以通过 onLayout 方法来获取它们的实际高度,并在渲染时将高度传递给 VirtualizedList 或 FlatList
class Item extends React.Component {
  state = {
    height: 0,
  };

  onLayout = (event) => {
    const { height } = event.nativeEvent.layout;
    this.setState({ height });
    this.props.onItemLayout(this.props.index, height);
  };

  render() {
    return (
      <View onLayout={this.onLayout}>
        <Text>{this.props.item.title}</Text>
        <Image source={this.props.item.image} />
      </View>
    );
  }
}

class MyList extends React.Component {
  itemHeights = [];

  onItemLayout = (index, height) => {
    this.itemHeights[index] = height;
  };

  getItemLayout = (data, index) => {
    return {
      length: this.itemHeights[index] || 0,
      offset: this.itemHeights.slice(0, index).reduce((a, b) => a + b, 0),
      index,
    };
  };

  render() {
    return (
      <VirtualizedList
        data={this.props.data}
        renderItem={({ item, index }) => (
          <Item item={item} index={index} onItemLayout={this.onItemLayout} />
        )}
        getItemCount={(data) => data.length}
        getItem={(data, index) => data[index]}
        getItemLayout={this.getItemLayout}
      />
    );
  }
}

在上面的代码中,我们封装了一个 Item 组件,它会在布局完成后通过 onLayout 方法获取自身的实际高度,并通过 onItemLayout 回调将高度传递给父组件。父组件 MyList 内部维护了一个 itemHeights 数组,用于缓存所有列表项的高度。同时,我们还实现了 getItemLayout 方法,根据 itemHeights 数组计算每个列表项的布局信息。

这种方式的优点是可以精确地获取每个列表项的实际高度,即使列表项高度不固定也能实现完美的自适应。然而,它也存在一些局限性:

  • 由于需要额外的高度计算和缓存,因此在性能上会有一定的开销。
  • 当列表项个数非常多时,itemHeights 数组可能会占用较大的内存空间。
  1. 利用占位组件实现列表项高度的预估。在一些场景下,我们可以预先估算列表项的平均高度,将这个估算高度作为列表项的占位高度,等到列表项真正渲染时再更新为实际高度。这样,即使在初次渲染时,列表项高度也能保持相对准确,不会出现大幅度的跳动。
class MyList extends React.PureComponent {
  state = {
    itemHeights: new Array(this.props.data.length).fill(200),
  };

  onItemLayout = (index, height) => {
    const itemHeights = [...this.state.itemHeights];
    itemHeights[index] = height;
    this.setState({itemHeights});
  }

  getItemLayout = (data, index) => {
    return {
      length: this.state.itemHeights[index],
      offset: this.state.itemHeights.slice(0, index).reduce((a, b) => a + b, 0),
      index,
    };
  }

  render() {
    return (
      <VirtualizedList
        data={this.props.data}
        renderItem={({item, index}) => (
          <Item
            item={item}
            index={index}
            onItemLayout={this.onItemLayout}
            estimatedHeight={this.state.itemHeights[index]}
          />
        )}
        getItemCount={data => data.length}
        getItem={(data, index) => data[index]}
        getItemLayout={this.getItemLayout}
      />
    );
  }
}

class Item extends React.PureComponent {
  render() {
    const {item, estimatedHeight, onItemLayout} = this.props;
    return (
      <View onLayout={({nativeEvent}) => onItemLayout(this.props.index, nativeEvent.layout.height)}>
        <View style={{height: estimatedHeight}}>
          <Text>{item.title}</Text>
          <Image source={item.image} />
        </View>
      </View>
    );
  }
}

在上面的代码中,我们使用一个 200 像素的估算高度来初始化 itemHeights 数组。在渲染列表项时,我们将这个估算高度传递给 Item 组件,作为列表项的占位高度。Item 组件内部渲染一个占位元素,高度等于估算高度,从而在初次渲染时,列表项整体高度能够保持稳定。等到列表项布局完成后,我们再通过 onLayout 方法获取实际高度,并更新 itemHeights 数组。

这种方式的优点是可以显著减少列表项渲染时的跳动,在视觉上给人更加连贯顺滑的感受。同时,它也兼顾了性能,避免了大量的重排重绘。然而,它也存在一些局限性:

  • 估算高度难以做到十分精确,因此在某些场景下,仍然可能出现轻微的跳动。
  • 对于高度差异很大的列表项,该方案的效果可能不太理想。

四、React Native 复杂列表的性能优化策略及其具体实践

除了高度自适应外,React Native 复杂列表的性能优化,还需要从以下几个方面入手:

  1. 使用 PureComponent 和 React.memo 减少不必要的渲染。在 React Native 中,组件的重新渲染是影响性能的主要因素之一。为了避免不必要的渲染,我们可以将列表项组件继承自 PureComponent,或者使用 React.memo 对其进行包装。这样,只有当 props 发生变化时,列表项才会重新渲染。
class Item extends React.PureComponent {
  render() {
    // ...
  }
}

// 或者
const Item = React.memo(function Item(props) {
  // ...
});
  1. 使用唯一且稳定的 key 属性。在渲染列表时,每个列表项都需要一个唯一的 key 属性,以便 React Native 能够准确地识别和复用列表项。如果 key 不唯一或者不稳定,就可能导致大量的重新渲染和重新创建,从而影响性能。因此,我们需要为列表项选择合适的 key,最好是来自数据本身的 id 字段。
<VirtualizedList
  data={this.props.data}
  renderItem={({item}) => <Item item={item} />}
  keyExtractor={item => item.id}
/>
  1. 尽量避免在列表项中使用箭头函数和匿名函数。在列表项中使用箭头函数或匿名函数,会导致每次渲染时都创建新的函数实例,从而触发列表项的重新渲染。为了避免这种情况,我们可以将事件处理函数提取到列表项组件的外部,或者使用类方法来定义事件处理函数。
class Item extends React.PureComponent {
  onPress = () => {
    this.props.onItemPress(this.props.item);
  }

  render() {
    return <TouchableOpacity onPress={this.onPress}>...</TouchableOpacity>;
  }
}
  1. 在列表项中延迟加载或者懒加载非关键渲染路径上的组件和数据。对于一些复杂的列表项,我们可以将其分解为多个部分,关键部分优先渲染,非关键部分延迟加载。这样可以避免一次性渲染大量的内容,从而提高列表的响应速度。常见的延迟加载技术包括:
  2. 渐进式图片加载。在列表项中显示图片时,先展示一个占位图,然后在后台加载真实图片,加载完成后再替换占位图。这样可以避免图片加载阻塞列表渲染。
  3. 懒加载列表项内容。在列表项首次出现在屏幕上时,只渲染关键内容,等列表项完全展示后,再去加载剩余内容。比如 IM 聊天中的语音消息,可以先显示一个语音 icon,等用户点击后再去加载语音内容。
  4. 使用 InteractionManager 将耗时操作移到非关键渲染路径InteractionManager 允许我们在关键交互完成后,再去执行一些耗时操作,从而避免这些操作阻塞用户交互。比如在复杂列表初次渲染后,我们可以在 InteractionManager 的回调中去加载列表数据。

下面是一个渐进式图片加载的示例:

function ProgressiveImage(props) {
  const [loadedSrc, setLoadedSrc] = useState(null);
  const ref = useRef();

  useEffect(() => {
    if (ref.current) {
      ref.current.onload = () => setLoadedSrc(props.src);
    }
  }, [props.src, ref]);

  return (
    <View style={props.style}>
      {loadedSrc ? (
        <Image style={props.style} source={{uri: loadedSrc}} />
      ) : (
        <Image
          ref={ref}
          style={props.style}
          source={props.placeholder}
          blurRadius={1}
        />
      )}
    </View>
  );
}
class MyList extends React.Component {
  state = {
    data: [],
  };

  componentDidMount() {
    InteractionManager.runAfterInteractions(() => {
      fetchData().then(data => this.setState({data}));
    });
  }

  render() {
    return <VirtualizedList data={this.state.data} />;
  }
}

五、React Native 列表与原生列表的差异及其优劣势比较

  在 App 开发中,除了使用 React Native 实现列表外,我们还可以选择原生的列表组件,比如 iOS 的 UITableView 和 Android 的 RecyclerView。那么,React Native 列表与原生列表相比,有哪些差异呢?各自的优劣势又是什么?

首先,从开发效率来看,React Native 列表无疑更胜一筹。使用 React Native,我们可以用一套代码同时适配 iOS 和 Android 两端,不仅可以显著减少开发和维护成本,还能保证双端表现的一致性。而使用原生列表,我们就需要分别编写 iOS 和 Android 两套代码,工作量要大很多。

其次,从性能角度来看,原生列表通常会更加流畅和高效。原生列表组件是直接使用平台自带的渲染引擎,能够最大限度地发挥设备的性能。而 React Native 列表是在 JavaScript 线程和原生主线程之间进行通信,这就不可避免地带来了一些性能损耗。特别是在列表项非常复杂,或者数据量非常大时,React Native 列表可能会出现卡顿和掉帧。

再次,从功能扩展性来看,原生列表可以支持更多自定义的功能和交互,比如 iOS 的 UITableViewCell 可以轻松实现左滑删除、右滑编辑等手势操作。而在 React Native 中,实现这些功能就需要额外的开发成本。当然,React Native 也有其独特的优势,比如 React 生态提供了大量优质的第三方组件,使用它们可以快速搭建起复杂的列表页面。

综合来看,React Native 列表与原生列表其实是相辅相成的,我们可以根据实际情况选择合适的技术方案。对于大多数常规的列表需求,使用 React Native 完全可以满足。但是对于对性能和体验要求非常高的场景,比如 IM 聊天、电商首页等,使用原生列表可以获得更好的效果。理想的做法是,将 React Native 与原生列表结合起来,发挥各自的优势:在跨平台的基础上引入原生列表,既提高了开发效率,又兼顾了用户体验。

小结

 本文从复杂列表样式、开发方式、高度自适应和性能优化等多个角度,全面探讨了 React Native 复杂列表的开发和优化策略。我们还将 React Native 列表与原生列表进行了对比,分析了彼此的优劣和适用场景。可以看到,React Native 凭借其高效的开发模式和灵活的扩展能力,已经成为移动端列表开发的重要选择。

作为一名 React Native 开发工程师,掌握复杂列表的实现和优化,是非常重要且必要的。我们需要在实践中不断积累经验,研究新的技术方案,力求为用户提供流畅、顺滑的列表体验。这不仅需要扎实的编程功底,还需要多方面的综合素质,比如产品思维、设计理念等。

  展望未来,相信 React Native 在列表领域还将不断突破创新,为开发者带来更多惊喜。让我们拭目以待吧!

以上就是我对 React Native 复杂列表开发和优化的一些思考和总结,希望对大家有所帮助。也欢迎大家留言交流,分享你的经验和见解!