Skip to content
On this page

FlatList 虚拟列表

注:

  • 需要给 FlatList 组件一个固定高度,通过 CSS 设置 height。
  • 如果列表内容存在不固定宽高的图片,由于图片加载的机制,每个 item 的 DOM 渲染了,图片可能还未加载,导致每个 item 的位置计算错误,所以作为调用方,需要将未加载的图片用样式固定住。在已知宽高比(比如常见的正方形商品图),可以使用 Image 组件内置 aspect-ratio 来固定图片宽高。
  • 该组件使用的是 Virtual List 虚拟列表技术,将渲染的数据控制在一定范围内,在海量数据滚动中达到更好的性能和体验。
  • 如果数据量过大(大几百以上),强烈建议设置 item-size,未设置时使用动态计算高度会带来渲染性能的下降。
vue
<script setup lang="ts">
import { reactive, ref } from 'vue'
import {
  TaFlatList,
  type FlatListOnEndReached,
  type FlatListOnRefreshing,
  type FlatListOnVisibleItemsChange,
  showToast,
  type ViewPosition
} from '@/index'

interface ExpList {
  id: number
  text: string
}

// 数据初始化
const list = reactive<ExpList[]>([])
const largeList = reactive<ExpList[]>([])
for (let i = 0; i < 100; i++) {
  list.push({
    id: i + 1,
    text: `${i + 1} 个列表`
  })
}
for (let i = 0; i < 100000; i++) {
  largeList.push({
    id: i + 1,
    text: `${i + 1} 个列表`
  })
}

// 瀑布流
function getItemSize(index: number) {
  return 50 + (index % 10) * 2
}

// 下拉刷新
const onRefreshing: FlatListOnRefreshing = (res, done) => {
  setTimeout(() => {
    showToast({
      title: `刷新成功`,
      type: 'success'
    })
    done()
  }, 2000)
}

// 加载更多
const lowerLoading = ref(true)
const loadList = reactive<ExpList[]>([])
function getLoadList() {
  for (let i = loadList.length, len = loadList.length + 10; i < len; i++) {
    loadList.push({
      id: i + 1,
      text: `${i + 1} 个列表`
    })
  }
}
getLoadList()
const onLoadMore: FlatListOnEndReached = res => {
  console.log('end-reached', res)

  const max = 100

  if (loadList.length >= max) {
    return
  }

  setTimeout(() => {
    getLoadList()
    showToast({
      title: `加载成功`,
      type: 'success'
    })

    if (loadList.length >= max) {
      lowerLoading.value = false
    }
  }, 500)
}

// 事件监听
const onVisibleItemsChange: FlatListOnVisibleItemsChange = ({ items }) => {
  console.log('visible-items-change', items)

  items.forEach(({ index, visible }) => {
    index === 49 && showToast(`index: ${index}, visable: ${visible}`)
  })
}
const onEndReached: FlatListOnEndReached = res => {
  console.log('end-reached', res)
  showToast(`到底了`)
}

// 方法调用
const methodList = ref<InstanceType<typeof TaFlatList>>()
function scrollToIndex(index: number, viewPosition: ViewPosition = 0) {
  methodList.value?.scrollToIndex({ index, viewPosition })
}
function scrollTo(offset: number) {
  methodList.value?.scrollTo({ offset })
}
function scrollToEnd(animated: boolean) {
  methodList.value?.scrollToEnd(animated)
}
</script>

<script lang="ts">
export default {
  name: 'ExpFlatList'
}
</script>

<template>
  <ta-group title="基础用法">
    <ta-flat-list class="exp-flatList-box" :ids="list.map(v => v.id)">
      <template #default="{ index }">
        <div class="exp-flatList-item">
          {{ list[index].text }}
        </div>
      </template>
    </ta-flat-list>
  </ta-group>
  <ta-group title="水平列表">
    <ta-flat-list
      class="exp-flatList-box"
      :ids="list.map(v => v.id)"
      :itemSize="140"
      initialHorizontal
    >
      <template #default="{ index }">
        <div class="exp-flatList-item">
          {{ list[index].text }}
        </div>
      </template>
    </ta-flat-list>
  </ta-group>
  <ta-group title="开启下拉刷新">
    <ta-flat-list
      class="exp-flatList-box"
      :ids="list.map(v => v.id)"
      :itemSize="50"
      :enablePullRefresh="true"
      @refreshing="onRefreshing"
    >
      <template #default="{ index }">
        <div class="exp-flatList-item">
          {{ list[index].text }}
        </div>
      </template>
    </ta-flat-list>
  </ta-group>
  <ta-group title="展示底部加载更多提示">
    <ta-flat-list
      class="exp-flatList-box"
      :ids="loadList.map(v => v.id)"
      :lowerLoading="lowerLoading"
      @endReached="onLoadMore"
    >
      <template #default="{ index }">
        <div class="exp-flatList-item">
          {{ list[index].text }}
        </div>
      </template>
    </ta-flat-list>
  </ta-group>
  <ta-group title="分割线(#separator)">
    <ta-flat-list class="exp-flatList-box" :ids="list.map(v => v.id)">
      <template #default="{ index }">
        <div class="exp-flatList-item">
          {{ list[index].text }}
        </div>
      </template>
      <template #separator="{ index }">
        <div class="exp-flatList-item-separator" v-if="index < list.length - 1"></div>
      </template>
    </ta-flat-list>
  </ta-group>
  <ta-group title="瀑布流">
    <ta-flat-list
      class="exp-flatList-box"
      :ids="list.map(v => v.id)"
      :itemSize="getItemSize"
      :initialWaterfallCount="3"
      ref="demo"
    >
      <template #default="{ index }">
        <div class="exp-flatList-item" :class="['color-' + (index % 10)]">
          {{ list[index].text }}
        </div>
      </template>
    </ta-flat-list>
  </ta-group>
  <ta-group title="事件监听(end-reached/visible-items-change)">
    <ta-flat-list
      class="exp-flatList-box"
      :ids="list.map(v => v.id)"
      :itemSize="50"
      @endReached="onEndReached"
      @visibleItemsChange="onVisibleItemsChange"
    >
      <template #default="{ index }">
        <div class="exp-flatList-item">
          {{ list[index].text }}
        </div>
      </template>
    </ta-flat-list>
  </ta-group>
  <ta-group title="Slot empty">
    <ta-flat-list class="exp-flatList-box" :ids="[]" :itemSize="50">
      <template #empty>
        <ta-empty description="暂无列表"></ta-empty>
      </template>
    </ta-flat-list>
  </ta-group>
  <ta-group title="Method">
    <ta-flat-list
      class="exp-flatList-box"
      :ids="largeList.map(v => v.id)"
      :itemSize="50"
      ref="methodList"
    >
      <template #default="{ index }">
        <div class="exp-flatList-item" :class="['color-' + (index % 10)]">
          {{ largeList[index].text }}
        </div>
      </template>
    </ta-flat-list>
    <ta-cell label="scrollToIndex({ index: 49999 })" isLink @click="scrollToIndex(49999)"></ta-cell>
    <ta-cell label="同上加 viewPosition=0.5" isLink @click="scrollToIndex(49999, 0.5)"></ta-cell>
    <ta-cell label="同上加 viewPosition=1" isLink @click="scrollToIndex(49999, 1)"></ta-cell>
    <ta-cell label="scrollTo({ offset: 200 })" isLink @click="scrollTo(200)"></ta-cell>
    <ta-cell label="scrollToEnd(true)" isLink @click="scrollToEnd(true)"></ta-cell>
  </ta-group>
</template>

Import

js
import { TaFlatList } from 'tantalum-ui-mobile'

具体的引入方式可以参考引入组件

Import Type

组件导出的类型定义:

ts
import type {
  FlatListOnRefreshing,
  FlatListOnScroll,
  FlatListOnEndReached,
  FlatListOnVisibleItemsChange,
  FlatListRef
} from 'tantalum-ui-mobile'

Props

属性类型默认值必填说明
ids(string | number)[]id 列表,会在渲染的时候作为参数回调
horizontalbooleanfalse设置为 true 则变为水平布局模式
item-sizenumber | (index: number) => number设置列表项尺寸(垂直布局下指高度,水平布局下指宽度),推荐设置,提升性能
end-reached-thresholdnumber0.5决定当距离内容最底部还有多远时触发 onEndReached 回调。注意此参数是一个比值而非像素单位。比如,0.5 表示距离内容最底部的距离为当前列表可见长度的 50%时触发
enable-pull-refreshbooleanfalse是否开启下拉刷,如果时水平列表则为左拉刷新,搭配 refreshing 事件使用新
lower-loadingbooleanfalse开启后会在列表最后展示加载更多效果,配合 end-reached 事件加载无限列,当多次加载完所有数据后设置为 false 关闭
initial-waterfall-countnumber1开启瀑布流展示并设置瀑布流列数,支持 1 ~ 5 列,1 就是没开
approved-item-visible-scalenumber0.5可选 0-1, 主要是配合 visible-items-change 事件使用,0.5 表示每项需要展示超过自身 50%才认为是可视 visible=true,在一些数数据统计和展示触发行为常用
preLoadnumber1.5预加载屏数,1.5 标识在在可视窗口之外再预加载大于 1.5 个窗口的数据,防止滚动过程中产生的白屏。预加载的屏数设置越高,加载的数据越多,高速滚动产生白屏的几率越小,但是相应性能消耗更大

Events

事件描述回调函数参数TypeScript 函数
visible-items-change列表项可视情况变化时触发payload: { items: { index: number, visible: boolean }[] }FlatListOnVisibleItemsChange
end-reached滚动到末尾时触发payload: { distanceFromEnd: number } 其中 distanceFromEnd 为距离末尾的距离,单位 pxFlatListOnEndReached
scroll滚动时触发payload: { scrollLeft: number, scrollTop: number, scrollWidth: number, scrollHeight: number }FlatListOnScroll
refreshing下拉刷新时触发payload: ({ pullDirection: 'up' | 'right' | 'down' | 'left' }, done: () =>void) 其中 pullDirection 指下拉的方向,done 指刷新完毕回调的函数FlatListOnRefreshing

visible-items-change 的 items 参数

item类型说明
visibleboolean这里的可视情况受到 approved-item-visible-scale 字段影响,可以配置它来达到你认可的可视情况
indexnumber第 index 项

Slots

列表项(#default)

vue
<ta-flat-list>
 <template #default="{ id, index }">
  {{ index }} : {{ id }}
 </template>
</ta-flat-list>

列表为空(#empty)

vue
<ta-flat-list>
  <template #empty>暂无数据</template>
  ...
</ta-flat-list>

分割线(#separator)

vue
<ta-flat-list>
  <template #separator>
    <div class="line"></div>
  </template>
</ta-flat-list>

注: itemSize 设定值需要把分割线也考虑进去。

前置(#header)

vue
<ta-flat-list>
  <template #header></template>
  ...
</ta-flat-list>

也可以传入 Icon,比如常见的搜索。

常见的方式是可以通过 footer 插槽来定义底部效果,替代默认的 lowerLoading 来实现无限滚动的加载效果。

vue
<ta-flat-list>
  ...
  <template #footer>暂时没有更多了</template>
</ta-flat-list>

Methods

方法名说明参数
scrollToIndex将位于指定位置的元素滚动到可视区的指定位置({ index: number, animated: boolean, viewPosition: ViewPosition }) => void
scrollTo滚动列表到指定的偏移,单位 px({ offset: number, animated: boolean }) => void
scrollToEnd滚动到底部( animated: boolean ) => void
recordInteraction主动通知列表发生了一个事件,以使列表重新计算可视区域() => void

scrollToIndex 的参数

属性类型默认值必填说明
indexnumber列表第 index 项滚动到可视区的指定位置
animatedbooleantrue滚动过程中是否使用过度动画
viewPositionstring'start''start'/'center'/'end'/0/0.5/1 显示在屏幕的头部/中间/末尾位置