title: 长列表渲染(虚拟列表)

在典型的 Taro 应用中,正常的列表渲染遵循以下的逻辑:

  1. 生成或从远程加载数据
  2. 把数据加入框架的响应式数据中
  3. 框架使用 diff 算法或其它机制根据数据的不同尝试全量更新视图
  4. Taro 运行时捕获框架的更新请求更新视图

如果按照此逻辑,当第一步我们生成或加载的数据量非常大时就可能会产生严重的性能问题,导致视图无法响应操作一段时间。为了解决这个问题,我们可以采用另一种方式:比起全量渲染数据生成的视图,可以只渲染当前可视区域(visable viewport)的视图,非可视区域的视图在用户滚动到可视区域再渲染:

virtual-list

React/Nerv

使用 React/Nerv 我们可以直接从 @tarojs/components/virtual-list 引入虚拟列表(VirtualList)组件:

  1. import VirtualList from '@tarojs/components/virtual-list'

一个最简单的长列表组件会像这样,VirtualList 的 5 个属性都是必填项:

  1. function buildData (offset = 0) {
  2. return Array(100).fill(0).map((_, i) => i + offset);
  3. }
  4. const Row = React.memo(({ index, style, data }) => {
  5. return (
  6. <View className={index % 2 ? 'ListItemOdd' : 'ListItemEven'} style={style}>
  7. Row {index}
  8. </View>
  9. );
  10. })
  11. export default class Index extends Component {
  12. state = {
  13. data: buildData(0),
  14. }
  15. render() {
  16. const { data } = this.state
  17. const dataLen = data.length
  18. return (
  19. <VirtualList
  20. height={500} /* 列表的高度 */
  21. width='100%' /* 列表的宽度 */
  22. itemData={data} /* 渲染列表的数据 */
  23. itemCount={dataLen} /* 渲染列表的长度 */
  24. itemSize={100} /* 列表单项的高度 */
  25. >
  26. {Row} /* 列表单项组件,这里只能传入一个组件 */
  27. </VirtualList>
  28. );
  29. }
  30. }

无限滚动

实现无限滚动也非常简单,我们只需要在列表滚动到底部时,往列表尾部追加数据即可:

  1. const Row = React.memo(({ index, style, data }) => {
  2. return (
  3. <View className={index % 2 ? 'ListItemOdd' : 'ListItemEven'} style={style}>
  4. Row {index}
  5. </View>
  6. );
  7. })
  8. function buildData (offset = 0) {
  9. return Array(100).fill(0).map((_, i) => i + offset);
  10. }
  11. export default class Index extends Component {
  12. state = {
  13. data: buildData(0),
  14. }
  15. loading = false
  16. listReachBottom() {
  17. Taro.showLoading()
  18. // 如果 loading 与视图相关,那它就应该放在 `this.state` 里
  19. // 我们这里使用的是一个同步的 API 调用 loading,所以不需要
  20. this.loading = true
  21. setTimeout(() => {
  22. const { data } = this.state
  23. this.setState({
  24. data: data.concat(buildData(data.length))
  25. }, () => {
  26. this.loading = false;
  27. Taro.hideLoading()
  28. })
  29. }, 1000)
  30. }
  31. render() {
  32. const { data } = this.state
  33. const dataLen = data.length
  34. const itemSize = 100
  35. return (
  36. <VirtualList
  37. className='List'
  38. height={500}
  39. itemData={data}
  40. itemCount={dataLen}
  41. itemSize={itemSize}
  42. width='100%'
  43. onScroll={({ scrollDirection, scrollOffset }) => {
  44. if (
  45. // 避免重复加载数据
  46. !this.loading &&
  47. // 只有往前滚动我们才触发
  48. scrollDirection === 'forward' &&
  49. // 5 = (列表高度 / 单项列表高度)
  50. // 100 = 滚动提前加载量,可根据样式情况调整
  51. scrollOffset > ((dataLen - 5) * itemSize + 100)
  52. ) {
  53. this.listReachBottom()
  54. }
  55. }}
  56. >
  57. {Row}
  58. </VirtualList>
  59. );
  60. }
  61. }

props

children: ReactComponent

将要渲染的列表单项组件。组件的 props 有 4 个属性:

  • style: 单项的样式,样式必须传入组件的 style
  • data: 组件渲染的数据
  • index: 组件渲染数据的索引
  • isScrolling: 组件是否正在滚动,当 useIsScrolling 值为 true 时返回布尔值,否则返回 undefined

推荐使用 React.memoReact.PureComponent 或使用 shouldComponentUpdate() 来优化此组件,避免不必要的渲染。

itemCount: number

列表的长度。必填。

itemData: Array<any>

渲染数据。必填。

itemSize: number

列表单项的大小,垂直滚动时为高度,水平滚动时为宽度。必填。

height: number | string

列表的高度。当滚动方向为垂直时必填。

width: number | string

列表的宽度。当滚动方向为水平时必填。

className: string

根组件 CSS 类

style: Style

根组件的样式

initialScrollOffset: number = 0

初始滚动偏移值,水平滚动影响 scrollLeft,垂直滚动影响 scrollTop

innerElementType: ReactElement = View

列表内部容器组件类型,默认值为 View。此容器的 parentNodeScrollViewchildNodes 是列表。

innerRef: Ref | Function

列表内部容器组件的 ref。

layout: string = 'vertical'

滚动方向。vertical 为垂直滚动,horizontal 为平行滚动。默认为 vertical

onScroll: Function

列表滚动时调用函数,函数的第一个参数为对象,由三个属性构成:

  • scrollDirection,滚动方向,可能值为 forward 往前, backward 往后。
  • scrollOffset,滚动距离
  • scrollUpdateWasRequested, 当滚动是由 scrollTo()scrollToItem() 调用时返回 true,否则返回 false

onScrollNative: Function

调用平台原生的滚动监听函数。

overscanCount: number = 1

在可视区域之外渲染的列表单项数量,值设置得越高,快速滚动时出现白屏的概率就越小,相应地,每次滚动的性能会变得越差。

useIsScrolling: boolean

是否注入 isScrolling 属性到 children 组件。这个参数一般用于实现滚动骨架屏(或其它 placeholder) 时比较有用。

其它 ScrollView 组件的参数

除了以上参数,所有 ScrollView 组件的参数都可以传入 VirtualList 组件,冲突时优先使用以上文档描述的参数。

方法

通过 React.createRef() 创建 ref,挂载到 VirtualList 上可以访问到 VirtualList 的内部方法:

  1. export default class Index extends Component {
  2. state = {
  3. data: buildData(0),
  4. }
  5. list = React.createRef()
  6. componentDidMount() {
  7. const list = this.list.current
  8. list.scrollTo()
  9. list.scrollToItem()
  10. }
  11. render() {
  12. const { data } = this.state
  13. const dataLen = data.length
  14. return (
  15. <VirtualList
  16. height={500} /* 列表的高度 */
  17. width='100%' /* 列表的宽度 */
  18. itemData={data} /* 渲染列表数据 */
  19. itemCount={dataLen} /* 渲染列表的长度 */
  20. itemSize={100} /* 列表单项的高度 */
  21. ref={this.list}
  22. >
  23. {Row} /* 列表单项组件,这里只能传入一个组件 */
  24. </VirtualList>
  25. );
  26. }
  27. }

scrollTo(scrollOffset: number): void

滚动到指定的地点。

scrollToItem(index: number, align: string = "auto"): void

滚动到指定的条目。

第二参数 align 的值可能为:

  • auto: 尽可能滚动距离最小保证条目在可视区域中,如果已经在可视区域,就不滚动
  • smart: 条目如果已经在可视区域,就不滚动;如果有部分在可视区域,尽可能滚动距离最小保证条目在可视区域中;如果条目完全不在可视区域,那就滚动到条目在可视区域居中显示
  • center: 让条目在可视区域居中显示
  • end: 让条目在可视区域末尾显示
  • start: 让条目在可视区域末尾显示

Vue

在 Vue 中使用虚拟列表,我们需要在入口文件声明使用:

  1. // app.js 入口文件
  2. import Vue from 'vue'
  3. import VirtualList from '@tarojs/components/virtual-list'
  4. Vue.use(VirtualList)

一个最简单的长列表组件会像这样,virtual-list 的 5 个属性都是必填项:

  1. <! –– row.vue 单项组件 ––>
  2. <template>
  3. <view
  4. :class="index % 2 ? 'ListItemOdd' : 'ListItemEven'"
  5. :style="css"
  6. >
  7. Row {{ index }}
  8. </view>
  9. </template>
  10. <script>
  11. export default {
  12. props: ['index', 'data', 'css']
  13. }
  14. </script>
  15. <! –– page.vue 页面组件 ––>
  16. <template>
  17. <virtual-list
  18. wclass="List"
  19. :height="500"
  20. :item-data="list"
  21. :item-count="list.length"
  22. :item-size="100"
  23. :item="Row"
  24. width="100%"
  25. />
  26. </template>
  27. <script>
  28. import Row from './row.vue'
  29. function buildData (offset = 0) {
  30. return Array(100).fill(0).map((_, i) => i + offset)
  31. }
  32. export default {
  33. data() {
  34. return {
  35. Row,
  36. list: buildData(0)
  37. }
  38. },
  39. }
  40. </script>

无限滚动

实现无限滚动也非常简单,我们只需要在列表滚动到底部时,往列表尾部追加数据即可:

  1. <template>
  2. <virtual-list
  3. wclass="List"
  4. :height="500"
  5. :item-data="list"
  6. :item-count="dataLen"
  7. :item-size="itemHeight"
  8. :item="Row"
  9. width="100%"
  10. @scroll="onScroll"
  11. />
  12. </template>
  13. <script>
  14. import Row from './row.vue'
  15. function buildData (offset = 0) {
  16. return Array(100).fill(0).map((_, i) => i + offset)
  17. }
  18. export default {
  19. data() {
  20. return {
  21. Row,
  22. list: buildData(0),
  23. loading: false,
  24. itemHeight: 100
  25. }
  26. },
  27. computed: {
  28. dataLen () {
  29. return this.list.length
  30. }
  31. },
  32. methods: {
  33. listReachBottom() {
  34. Taro.showLoading()
  35. this.loading = true
  36. setTimeout(() => {
  37. const { data } = this.state
  38. this.setState({
  39. data: data.concat(buildData(data.length))
  40. }, () => {
  41. this.loading = false;
  42. Taro.hideLoading()
  43. })
  44. }, 1000)
  45. },
  46. onScroll({ scrollDirection, scrollOffset }) {
  47. if (
  48. // 避免重复加载数据
  49. !this.loading &&
  50. // 只有往前滚动我们才触发
  51. scrollDirection === 'forward' &&
  52. // 5 = (列表高度 / 单项列表高度)
  53. // 100 = 滚动提前加载量,可根据样式情况调整
  54. scrollOffset > ((this.dataLen - 5) * this.itemHeight + 100)
  55. ) {
  56. this.listReachBottom()
  57. }
  58. }
  59. }
  60. }
  61. </script>

props

item: VueComponent

将要渲染的列表单项组件。组件的 props 有 4 个属性:

  • css: 单项的样式,样式必须传入组件的 style
  • data: 组件渲染的数据
  • index: 组件渲染数据的索引
  • isScrolling: 组件是否正在滚动,当 useIsScrolling 值为 true 时返回布尔值,否则返回 undefined

itemCount: number

列表的长度。必填。

itemData: Array<any>

渲染数据。必填。

itemSize: number

列表单项的大小,垂直滚动时为高度,水平滚动时为宽度。必填。

height: number | string

列表的高度。当滚动方向为垂直时必填。

width: number | string

列表的宽度。当滚动方向为水平时必填。

wclass: string

根组件 CSS 类

wstyle: Style

根组件的样式

initialScrollOffset: number = 0

初始滚动偏移值,水平滚动影响 scrollLeft,垂直滚动影响 scrollTop

innerElementType: string = 'view'

列表内部容器组件类型,默认值为 view。此容器的 parentNodescroll-viewchildNodes 是列表。

layout: string = 'vertical'

滚动方向。vertical 为垂直滚动,horizontal 为平行滚动。默认为 vertical

v-on:scroll: Function

列表滚动时调用函数,函数的第一个参数为对象,由三个属性构成:

  • scrollDirection,滚动方向,可能值为 forward 往前, backward 往后。
  • scrollOffset,滚动距离
  • scrollUpdateWasRequested, 当滚动是由 scrollTo()scrollToItem() 调用时返回 true,否则返回 false

scrollNative: Function

调用平台原生的滚动监听函数。注意调用传递此函数时使用的是 v-bind 而不是 v-on

  1. <virtual-list
  2. wclass="List"
  3. :height="500"
  4. :item-data="list"
  5. :item-count="list.length"
  6. :item-size="100"
  7. :item="Row"
  8. width="100%"
  9. @scroll="onScroll"
  10. :scroll-native="onScrollNative"
  11. />

overscanCount: number = 1

在可视区域之外渲染的列表单项数量,值设置得越高,快速滚动时出现白屏的概率就越小,相应地,每次滚动的性能会变得越差。

useIsScrolling: boolean

是否注入 isScrolling 属性到 item 组件。这个参数一般用于实现滚动骨架屏(或其它 placeholder) 时比较有用。