虚拟列表作为一种常见的优化手段,可以拆分数据,缩减需要显示的组件数量,在显示大量数据时非常有用。例如 React Web 领域大名鼎鼎的 react-virtualized 和 React Native 自带的 FlatList。
本文将要介绍的是可以在 Taro3 中运行的 react-window。
react-window 工作原理#
react-window 是 react-virtualized 作者重写的新作品,更小,更快,抽象程度更高。
让我们先从一个简单的示例开始,对 react-window
有一个直观的认识:
import { FixedSizeList } from 'react-window'
const Demo = () => (
<FixedSizeList height={300} itemCount={1000} itemSize={30}>
{({ index, style }) => <div style={style}>行号:{index + 1}</div>}
</FixedSizeList>
)
行号:1
行号:2
行号:3
行号:4
行号:5
行号:6
行号:7
行号:8
行号:9
行号:10
行号:11
行号:12
FixedSizeList
用于显示固定行高的列表,指定高度(height
)确定视口,通过长度(itemCount
)和行高(itemSize
)计算出可见区域,每一行所需显示的内容通过 Render Props 由用户自己决定。
上面的例子中,我们指定列表的高度为 300,行高为 30,虽然数据有 1000 条,实际渲染的只有 10 行。(算上视口外预加载行实际超过 10 行)
FixedSizeList
本身渲染后的 DOM 结构共分两层:
- 外部滚动元素,相当于灰色边框部分。
- 内部占位元素,实际是一个占位区块。在上例中这个区块的高度是 30000,滚动时这个区块会整体向上移动,只露出 10 行。
想象一下电梯的样子。
Taro3 整合#
FixedSizeList
提供了两个属性 outerElementType
和 innerElementType
,用于自定义外部滚动元素和内部占位元素,对照 @tarojs/components
很容易发现此处可以用 <ScrollView>
和 <View>
进行替换。
其中 innerElementType
比较简单,直接替换即可,innerElementType={View}
。
对于 outerElementType
对应的 <ScrollView>
,经过尝试发现要满足以下 3 个条件:
- 向下传递
ref
onScroll
事件参数要具备 {currentTarget: {clientWidth, clientHeight}}
- 允许滚动。(因为
<ScrollView>
默认是不能滚动的。。。)
有了这些条件,实现起来就简单了。
OuterScrollView.tsx
import React from 'react'
import { ScrollView } from '@tarojs/components'
import { ScrollViewProps } from '@tarojs/components/types/ScrollView'
const OuterScrollView = React.forwardRef<any, ScrollViewProps>((props, ref) => {
const { style, onScroll, ...rest } = props
return (
<ScrollView
ref={ref}
style={style}
scrollY
onScroll={(e) => {
onScroll({
...e,
currentTarget: { ...e.detail, clientWidth: style.width, clientHeight: style.height },
})
}}
{...rest}
/>
)
})
FixedSizeList.tsx
import React from 'react'
import { View } from '@tarojs/components'
import { FixedSizeList as List, FixedSizeListProps } from 'react-window'
import { OuterScrollView } from './OuterScrollView'
export const FixedSizeList = React.forwardRef<List, FixedSizeListProps>((props, ref) => {
return <List ref={ref} outerElementType={OuterScrollView} innerElementType={View} {...props} />
})
真实案例#
在 taro3 项目中,安装两个依赖模块。
npm i @tarojsx/library @tarojsx/polyfill
更新 config/index.js 配置,目的是向运行环境中注入 requestAnimationFrame
。
/config/index.js
const { TaroProvidePlugin } = require('@tarojsx/polyfill/dist/plugins')
const config = {
mini: {
webpackChain(chain, webpack) {
chain.plugin('taroProviderPlugin').use(TaroProvidePlugin)
},
},
}
编写页面组件
/src/pages/list.tsx
import React, { useMemo, useState, useEffect } from 'react'
import Taro from '@tarojs/taro'
import { View } from '@tarojs/components'
import { FixedSizeList } from '@tarojsx/library/dist/react-window'
export default () => {
const { windowWidth, windowHeight } = useMemo(() => Taro.getSystemInfoSync(), [])
const [data, setData] = useState<{ name: string }[]>([])
useEffect(() => {
;(async () => {
const res = await Taro.request({
url: 'https://example.com/api/users',
})
setData(res.data)
})()
}, [])
return (
<View>
<FixedSizeList width={windowWidth} height={windowHeight} itemCount={data.length} itemSize={50}>
{({ index, style }) => (
<View key={index} style={style}>
{data[index].name}
</View>
)}
</FixedSizeList>
</View>
)
}
虚拟列表本质上是在滚动时不断更新视口中的行,由于屏幕尺寸的限制,同时显示的行数不会太多,即使是 60FPS 的滚动对于 React
也没什么压力。不过在 Taro3
里,由于取代了 ReactDOM
,UI 更新是通过小程序的 setData
实现的,在快速滚动虚拟列表时,setData
会以每秒 60 次的速度被调用,低端机型极易出现卡顿。如果对 onScroll
事件进行节流,又会造成列表更新不及时,带来用户感观上的延迟。
本文简单的介绍了固定行高列表,事实上 react-window 还支持横向滚动的列表,以及横竖都可滚动的表格,使用方法和固定行高列表类似,此处不再重复。
另外 react-window 还支持可变行高列表,因为小程序获取元素的宽高是异步操作,性能和准确性都无法保障,实际价值不大。