Skip to main content

React Native RecyclerListView 解决列表卡顿问题

React Native (RN) 是 Facebook 开源的跨平台前端解决方案,允许开发者用 React 去开发终端 App。

RN 有 ScrollView 组件来渲染列表数据,ScrollView 是所有组件一次渲染的; 所以 RN 推出了 FlatList,优化可视范围外的组件渲染,然而遇到大量数据时,FlatList 同样无能为力,此时滑动可能感觉不出卡顿,但是逻辑计算那边几乎卡死了,会看到列表正常滑动但是按钮点了没反应的现象。

简介

社区有一个列表组件的解决方案 Flipkart/recyclerlistview。它可以通过数据对等比较和子组件(Item)的宽高,决定是否渲染,或更新渲染。

以最简单的例子,渲染 10 个高度 100 的组件,但是 RecyclerListView 容器高度为 300,则可能只有3个组件能被渲染出来。按这个效果,渲染大列表的问题就迎刃而解了。

安装

本文用的 RecyclerListView 版本 = 3.0.5

RecyclerListView 是完全 JS 代码,不需要链接原生代码包。

yarn add recyclerlistview

关键 Props

RecyclerListView 必须传入的参数,用来计算是否渲染。

Props 描述
layoutProvider 布局配置,定义每个子组件的宽高
dataProvider 数据配置,定义组件数据
rowRenderer 渲染方法

Example

RecyclerListView 默认视所有的 Item 为 行级元素,即优先横排,宽度不够时做换行。

锁定宽高,水平布局(Row)

直接看 Expo Snack 示例

锁定宽高,组件不会占满屏幕

这个例子渲染一个包含200条数据的 RecyclerListView,但是 LayoutProvider 已经写死了每个 Item 的宽高是 100,在宽度 375 的屏幕上没有占满。

// App.tsx
import React from 'react';
import { View, Text } from 'react-native';
import { RecyclerListView, DataProvider, LayoutProvider } from 'recyclerlistview';


const RecyclerListViewExample = () => {
  const data = [...Array(200).keys()].map(id => `Item ${id}`)
  const layoutProvider = new LayoutProvider(
    (itemIndex) => {
      return 'MY_LAYOUT_TYPE'
    },
    (itemType, dimension) => {
      switch (itemType) {
        case 'MY_LAYOUT_TYPE':
          dimension.height = 100;
          dimension.width = 100;
      }
    }
  );
  const [dataProvider, setDataProvider] = React.useState(new DataProvider(
    (item1, item2) => item1 !== item2,
  ).cloneWithRows(data))
  
  return <RecyclerListView 
    layoutProvider={layoutProvider}
    dataProvider={dataProvider}
    rowRenderer={(itemType, item) => (
      <View style={{ flex: 1, backgroundColor: 'cyan' }}>
        <Text>{item}</Text>
      </View>
    )}
  />
}

export default RecyclerListViewExample;

锁定高度,宽度自动(Grid)

直接看 Expo Snack 示例

RecyclerListView 自带一个栅格布局库 GridLayoutProvider,可以轻松实现栅格列表。

宽度自动填满,高度仍然固定

栅格布局下不需要确定宽度(width)了,但仍然要确定高度(height)。

// App.tsx
import React from 'react';
import { View, Text } from 'react-native';
import { RecyclerListView, DataProvider, GridLayoutProvider } from 'recyclerlistview';


const RecyclerListViewExample = () => {
  const data = [...Array(200).keys()].map(id => `Item ${id}`)
  const layoutProvider = new GridLayoutProvider(
    2, // 每行 2 格
    (itemIndex) => 'MY_LAYOUT_TYPE',
    (itemIndex) => 1, // Item 占 1 格
    () => 100, // Item 高度 100
  );
  const [dataProvider, setDataProvider] = React.useState(new DataProvider(
    (item1, item2) => item1 !== item2,
  ).cloneWithRows(data))
  
  return <RecyclerListView 
    layoutProvider={layoutProvider}
    dataProvider={dataProvider}
    rowRenderer={(itemType, item) => (
      <View style={{ flex: 1, backgroundColor: 'cyan' }}>
        <Text>{item}</Text>
      </View>
    )}
  />
}

export default RecyclerListViewExample;

宽高都不知道

宽高都不知道的情况下,RecyclerListView 无法知道 Item 有没有离开可视范围。

如果不确定组件的实际宽高,但是能确定 Item 每次渲染都是同样的宽高,则可以用 View 的 onLayout 回调,拿到 Item 渲染后的实际宽高,再让 RecyclerListView 重新渲染。

const onLayout = e => {
  // 在回调中拿到宽高了
  const { width, height } = e.nativeEvent.layout;
}

const itemView = <View onLayout={onLayout} />

附录与引用

更新 DataProvider 的数据

对于 FlatList 的数据更新,React.useState([]) 比较常用。

RecyclerListView 要求的数据源是 DataProvider 对象。DataProvider 有 cloneWithRows 用来定义数据:

// 初始化数据源
const [dataProvider, setDataProvider] = React.useState(new DataProvider(
  (row1, row2) => row1 !== row2,
).cloneWithRows([1, 2, 3]))

// 更新数据源
const newDataProvider = dataProvider.cloneWithRows([4, 5, 6])
setDataProvider(newDataProvider)

官方推荐直接用现有的 DataProvider clone 一个新的数据源,以避免组件重新计算。

更新 DataProvider 后,列表自己滚动到某个 Item 的位置

Github Issue: Scroll offset not preserve when update re-render

RecyclerListView 默认在数据源更新后,将 scroll 滚动到某个 Item 的位置。

// 新建 LayoutProvider 时,关掉
const layoutProvider = new LayoutProvider(...);
// 关掉刷新后滚动
layoutProvider.shouldRefreshWithAnchoring = false;