React Native FlatList 深度解析:ListEmptyComponent 居中与 onEndReached 防抖封装实战

作者:晴川

时间:2026-10-22

浏览:296

本文深入解析 React Native 中 FlatList 组件的两个常见陷阱。首先分析 ListEmptyComponent 无法垂直居中的源码原因,通过 onLayout 获取高度并同步 State 解决 iOS 渲染时机差异导致的闪屏问题。其次探讨 onEndReachedThreshold 倍数机制引发的无限循环调用,提出通过 pageSize 取余判断及阈值优化进行二次封装的方案,提供稳定高效的列表处理策略。

文章目录

FlatList 核心功能回顾

在 React Native 开发中,FlatList 是官方推荐的高性能列表组件,作为 ListView 的升级版,它在性能表现上有了显著提升。因此,在实现列表功能时,强烈建议使用 FlatList,避免使用已废弃的 ListView 或性能较差的 ScrollView。接下来我们将回顾 FlatList 支持的核心功能。

  1. 具备完全跨平台运行的能力。
  2. 支持水平方向的布局模式。
  3. 允许配置行组件显示或隐藏时的回调事件。
  4. 支持自定义单独的头部组件。
  5. 支持自定义单独的尾部组件。
  6. 支持自定义行与行之间的分隔线样式。
  7. 支持下拉刷新功能。
  8. 支持上拉加载更多数据。
  9. 支持通过 ScrollToIndex 跳转到指定行。

FlatList 二次封装与空列表居中解决方案

本文不赘述基础用法,具体使用示例可参考 GitHub 仓库 https://github.com/xiehui999/helloReactNative。文章重点在于分享实际开发中遇到的主要坑点,并基于此对 FlatList 进行二次封装。

首先通过一个简单示例展开讨论。


    <FlatList
      data={this.state.dataList} extraData={this.state}
      refreshing={this.state.isRefreshing}
      onRefresh={() => this._onRefresh()}
      keyExtractor={(item, index) => item.id}
      ItemSeparatorComponent={() => <View style={{
        height: 1,
        backgroundColor: '#D6D6D6'
      }}/>}
      renderItem={this._renderItem}
      ListEmptyComponent={this.emptyComponent}/>
      
      
  //定义空布局
    emptyComponent = () => {
    return <View style={{
      height: '100%',
      alignItems: 'center',
      justifyContent: 'center',
    }}>
      <Text style={{
        fontSize: 16
      }}>暂无数据下拉刷新</Text>
    </View>
  }

在上述代码中,我们重点关注 ListEmptyComponent 属性,它用于定义无数据时的占位布局。通常我们希望提示信息居中显示,但直接运行后发现,文字虽然水平居中,却位于屏幕最上方,此时设置 height: '100%' 并未生效。即使尝试使用 flex: 1 填充剩余空间,依然无法达到垂直居中的效果。

为何设置无效?通过查阅源码(路径:react-native/Libraries/Lists)可以找到答案。在 FlatList 文件中搜索 ListEmptyComponent,发现其并未直接渲染,而是交由 render 方法处理。


 render() {
  if (this.props.legacyImplementation) {
   return (
    <MetroListView
     {...this.props}
     items={this.props.data}
     ref={this._captureRef}
    />
   );
  } else {
   return (
    <VirtualizedList
     {...this.props}
     renderItem={this._renderItem}
     getItem={this._getItem}
     getItemCount={this._getItemCount}
     keyExtractor={this._keyExtractor}
     ref={this._captureRef}
     onViewableItemsChanged={
      this.props.onViewableItemsChanged && this._onViewableItemsChanged
     }
    />
   );
  }
 }

render 方法内部根据 legacyImplementation 属性决定渲染 MetroListView(旧版 ScrollView 实现)还是 VirtualizedList(新版高性能实现)。我们需查看 VirtualizedList 相关代码。


  //省略部分代码
  const itemCount = this.props.getItemCount(data);
  if (itemCount > 0) {
    ....省略部分代码
  } else if (ListEmptyComponent) {
   const element = React.isValidElement(ListEmptyComponent)
    ? ListEmptyComponent // $FlowFixMe
    : <ListEmptyComponent />;
   cells.push(
    /* $FlowFixMe(>=0.53.0 site=react_native_fb,react_native_oss) This
     * comment suppresses an error when upgrading Flow's support for React.
     * To see the error delete this comment and run Flow. */
    <View
     key="$empty"
     onLayout={this._onLayoutEmpty}
     style={inversionStyle}>
     {element}
    </View>,
   );
  }

在源码中可以看到,定义的 ListEmptyComponent 被包裹在一个添加了 inversionStyle 样式的 View 中。


const inversionStyle = this.props.inverted
   ? this.props.horizontal
    ? styles.horizontallyInverted
    : styles.verticallyInverted
   : null;
   
样式:
verticallyInverted: {
  transform: [{scaleY: -1}],
 },
 horizontallyInverted: {
  transform: [{scaleX: -1}],
 },

该 inversionStyle 仅包含动画变换(如翻转),并未设置高度属性。因此,在 ListEmptyComponent 内部使用 height: '100%' 或 flex: 1 均无法撑开高度,导致布局异常。

要实现预期效果,需将高度设置为具体数值。由于 FlatList 默认通过 flex: 1 占满屏幕剩余高度,我们可以通过 onLayout 事件获取 FlatList 的实际高度,并将其赋值给 ListEmptyComponent 内部的 View。

代码调整如下:

iOS 端高度获取与 State 同步优化


//创建变量
fHeight = 0;

    <FlatList
      data={this.state.dataList} extraData={this.state}
      refreshing={this.state.isRefreshing}
      onRefresh={() => this._onRefresh()}
      keyExtractor={(item, index) => item.id}
      ItemSeparatorComponent={() => <View style={{
        height: 1,
        backgroundColor: '#D6D6D6'
      }}/>}
      renderItem={this._renderItem}
      onLayout={e => this.fHeight = e.nativeEvent.layout.height}
      ListEmptyComponent={this.emptyComponent}/>
      
      
  //定义空布局
    emptyComponent = () => {
    return <View style={{
      height: this.fHeight,
      alignItems: 'center',
      justifyContent: 'center',
    }}>
      <Text style={{
        fontSize: 16
      }}>暂无数据</Text>
    </View>
  }

经过上述调整,Android 端运行效果符合预期。但在 iOS 端表现不稳定,有时居中,有时仍置顶。原因是 iOS 上 onLayout 的调用时机与 Android 存在差异:在 iOS 上,当 emptyComponent 渲染时,onLayout 回调可能尚未触发,导致 fHeight 仍为初始值 0。

为了解决此问题,我们将测量得到的高度 fHeight 存入 State 中,以便在值更新后重新渲染组件。


state={
  fHeight:0
}

onLayout={e => this.setState({fHeight: e.nativeEvent.layout.height})}

然而,Android 端效果完美,iOS 端却出现了频繁闪屏。打印日志发现,fHeight 在 0 和测量值之间反复切换。由于我们只需要最终的测量值,因此需修改 onLayout 逻辑,仅在高度发生变化时更新 State。


           onLayout={e => {
             let height = e.nativeEvent.layout.height;
             if (this.state.fHeight < height) {
               this.setState({fHeight: height})
             }
           }}

经过上述优化,iOS 端最终完美实现了列表为空时的垂直居中显示。

onEndReached 循环调用陷阱与阈值优化

除了上述问题,onEndReached 也是另一个常见坑点。实现下拉加载时通常会用到它,这必然涉及 onEndReachedThreshold 属性。在 FlatList 中,onEndReachedThreshold 为 number 类型,表示触发 onEndReached 时距离底部的距离。需注意,FlatList 与 ListView 中该属性的含义不同:ListView 中单位为像素,默认值为 1000;而 FlatList 中为倍数(比值),默认值为 2。

按照常规逻辑,我们可能编写如下实现:


      <FlatList
        data={this.state.dataList}
        extraData={this.state}
        refreshing={this.state.isRefreshing}
        onRefresh={() => this._onRefresh()}
        ItemSeparatorComponent={() => <View style={{
          height: 1,
          backgroundColor: '#D6D6D6'
        }}/>}
        renderItem={this._renderItem}
        ListEmptyComponent={this.emptyComponent}
        onEndReached={() => this._onEndReached()}
        onEndReachedThreshold={0.1}/>

在 componentDidMount 中初始化加载第一页数据,下拉时执行 onEndReached 加载更多并更新 dataList。


  componentDidMount() {
    this._onRefresh()
  }

看似完美,但运行后发现 onEndReached 会循环调用(或多次执行),直至数据加载完毕。原因在于 _onRefresh 请求数据需要时间,在数据返回前 render 方法已执行,此时因无数据触发一次 onEndReached,导致逻辑上相当于加载了两次数据。

onEndReached 的执行次数取决于 onEndReachedThreshold 的值。若将其误解为像素值并设置为较大数值(如 100),将导致严重问题。个人建议将该值设置为较小的倍数,如 0.1,以控制触发频率。

FlatList 综合封装实现

基于以上分析,有必要对 FlatList 进行二次封装,以满足特定业务需求。


import React, {
  Component,
} from 'react'
import {
  FlatList,
  View,
  StyleSheet,
  ActivityIndicator,
  Text
} from 'react-native'
import PropTypes from 'prop-types';

export const FlatListState = {
  IDLE: 0,
  LoadMore: 1,
  Refreshing: 2
};
export default class Com extends Component {
  static propTypes = {
    refreshing: PropTypes.oneOfType([PropTypes.bool, PropTypes.number]),
  };
  state = {
    listHeight: 0,
  }

  render() {
    var {ListEmptyComponent,ItemSeparatorComponent} = this.props;
    var refreshing = false;
    var emptyContent = null;
    var separatorComponent = null
    if (ListEmptyComponent) {
      emptyContent = React.isValidElement(ListEmptyComponent) ? ListEmptyComponent : <ListEmptyComponent/>
    } else {
      emptyContent = <Text style={styles.emptyText}>暂无数据下拉刷新</Text>;
    }
    if (ItemSeparatorComponent) {
      separatorComponent = React.isValidElement(ItemSeparatorComponent) ? ItemSeparatorComponent :
        <ItemSeparatorComponent/>
    } else {
      separatorComponent = <View style={{height: 1, backgroundColor: '#D6D6D6'}}/>;
    }
    if (typeof this.props.refreshing === "number") {
      if (this.props.refreshing === FlatListState.Refreshing) {
        refreshing = true
      }
    } else if (typeof this.props.refreshing === "boolean") {
      refreshing = this.props.refreshing
    } else if (typeof this.props.refreshing !== "undefined") {
      refreshing = false
    }
    return <FlatList
      {...this.props}
      onLayout={(e) => {
        let height = e.nativeEvent.layout.height;
        if (this.state.listHeight < height) {
          this.setState({listHeight: height})
        }
      }
      }
      ListFooterComponent={this.renderFooter}
      onRefresh={this.onRefresh}
      onEndReached={this.onEndReached}
      refreshing={refreshing}
      onEndReachedThreshold={this.props.onEndReachedThreshold || 0.1}
      ItemSeparatorComponent={()=>separatorComponent}
      keyExtractor={(item, index) => index}
      ListEmptyComponent={() => <View
        style={{
          height: this.state.listHeight,
          width: '100%',
          alignItems: 'center',
          justifyContent: 'center'
        }}>{emptyContent}</View>}
    />
  }

  onRefresh = () => {
    console.log("FlatList:onRefresh");
    if ((typeof this.props.refreshing === "boolean" && !this.props.refreshing) ||
      typeof this.props.refreshing === "number" && this.props.refreshing !== FlatListState.LoadMore &&
      this.props.refreshing !== FlatListState.Refreshing
    ) {
      this.props.onRefresh && this.props.onRefresh()
    }

  };
  onEndReached = () => {
    console.log("FlatList:onEndReached");
    if (typeof this.props.refreshing === "boolean" || this.props.data.length == 0) {
      return
    }
    if (!this.props.pageSize) {
      console.warn("pageSize must be set");
      return
    }
    if (this.props.data.length % this.props.pageSize !== 0) {
      return
    }
    if (this.props.refreshing === FlatListState.IDLE) {
      this.props.onEndReached && this.props.onEndReached()
    }
  };


  renderFooter = () => {
    let footer = null;
    if (typeof this.props.refreshing !== "boolean" && this.props.refreshing === FlatListState.LoadMore) {
      footer = (
        <View style={styles.footerStyle}>
          <ActivityIndicator size="small" color="#888888"/>
          <Text style={styles.footerText}>数据加载中…</Text>
        </View>
      )
    }
    return footer;
  }
}
const styles = StyleSheet.create({
  footerStyle: {
    flex: 1,
    flexDirection: 'row',
    justifyContent: 'center',
    alignItems: 'center',
    padding: 10,
    height: 44,
  },
  footerText: {
    fontSize: 14,
    color: '#555555',
    marginLeft: 7
  },
  emptyText: {
    fontSize: 17,
    color: '#666666'
  }
})

在 propTypes 中,我们使用 oneOfType 对 refreshing 类型进行限定。若定义了 ListEmptyComponent,则使用自定义 View;同理,ItemSeparatorComponent 也支持自定义。

在下拉加载时,定义了 ListFooterComponent 以提示用户正在加载。refreshing 属性若为 boolean 类型,表示无下拉加载功能;若为 number 类型,则必须传入 pageSize。通过判断数据源长度与 pageSize 的余数是否为 0,来确定是否还有更多数据(仅当最后一次请求的数据量等于 pageSize 时才认为有更多数据,否则不回调 onEndReached)。上述封装逻辑简单清晰,此处不再赘述。以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章