Taro 長列表渲染(虛擬列表)

2021-09-23 21:10 更新

在典型的 Taro 應(yīng)用中,正常的列表渲染遵循以下的邏輯:

  1. 生成或從遠(yuǎn)程加載數(shù)據(jù)
  2. 把數(shù)據(jù)加入框架的響應(yīng)式數(shù)據(jù)中
  3. 框架使用 diff 算法或其它機(jī)制根據(jù)數(shù)據(jù)的不同嘗試全量更新視圖
  4. Taro 運(yùn)行時(shí)捕獲框架的更新請(qǐng)求更新視圖

如果按照此邏輯,當(dāng)?shù)谝徊轿覀兩苫蚣虞d的數(shù)據(jù)量非常大時(shí)就可能會(huì)產(chǎn)生嚴(yán)重的性能問題,導(dǎo)致視圖無法響應(yīng)操作一段時(shí)間。為了解決這個(gè)問題,我們可以采用另一種方式:比起全量渲染數(shù)據(jù)生成的視圖,可以只渲染**當(dāng)前可視區(qū)域(visable viewport)**的視圖,非可視區(qū)域的視圖在用戶滾動(dòng)到可視區(qū)域再渲染:

virtual-list

React/Nerv

使用 React/Nerv 我們可以直接從 @tarojs/components/virtual-list 引入虛擬列表(VirtualList)組件:

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

一個(gè)最簡單的長列表組件會(huì)像這樣,VirtualList 的 5 個(gè)屬性都是必填項(xiàng):

  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} /* 渲染列表?的數(shù)據(jù) */
  23. itemCount={dataLen} /* 渲染列表的長度 */
  24. itemSize={100} /* 列表單項(xiàng)的高度 */
  25. >
  26. {Row} /* 列表單項(xiàng)組件,這里只能傳入一個(gè)組件 */
  27. </VirtualList>
  28. );
  29. }
  30. }

無限滾動(dòng)

實(shí)現(xiàn)無限滾動(dòng)也非常簡單,我們只需要在列表滾動(dòng)到底部時(shí),往列表尾部追加數(shù)據(jù)即可:

  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 與視圖相關(guān),那它就應(yīng)該放在 `this.state` 里
  19. // 我們這里使用的是一個(gè)同步的 API 調(diào)用 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. // 避免重復(fù)加載數(shù)據(jù)
  46. !this.loading &&
  47. // 只有往前滾動(dòng)我們才觸發(fā)
  48. scrollDirection === 'forward' &&
  49. // 5 = (列表高度 / 單項(xiàng)列表高度)
  50. // 100 = 滾動(dòng)提前加載量,可根據(jù)樣式情況調(diào)整
  51. scrollOffset > ((dataLen - 5) * itemSize + 100)
  52. ) {
  53. this.listReachBottom()
  54. }
  55. }}
  56. >
  57. {Row}
  58. </VirtualList>
  59. );
  60. }
  61. }

props

children: ReactComponent

將要渲染的列表單項(xiàng)組件。組件的 props 有 4 個(gè)屬性:

  • style: 單項(xiàng)的樣式,樣式必須傳入組件的 style
  • data: 組件渲染的數(shù)據(jù)
  • index: 組件渲染數(shù)據(jù)的索引
  • isScrolling: 組件是否正在滾動(dòng),當(dāng) useIsScrolling 值為 true 時(shí)返回布爾值,否則返回 undefined

推薦使用 React.memoReact.PureComponent 或使用 shouldComponentUpdate() 來優(yōu)化此組件,避免不必要的渲染。

itemCount: number

列表的長度。必填。

itemData: Array

渲染數(shù)據(jù)。必填。

itemSize: number

列表單項(xiàng)的大小,垂直滾動(dòng)時(shí)為高度,水平滾動(dòng)時(shí)為寬度。必填。

height: number | string

列表的高度。當(dāng)滾動(dòng)方向?yàn)榇怪睍r(shí)必填。

width: number | string

列表的寬度。當(dāng)滾動(dòng)方向?yàn)樗綍r(shí)必填。

className: string

根組件 CSS 類

style: Style

根組件的樣式

initialScrollOffset: number = 0

初始滾動(dòng)偏移值,水平滾動(dòng)影響 scrollLeft,垂直滾動(dòng)影響 scrollTop。

innerElementType: ReactElement = View

列表內(nèi)部容器組件類型,默認(rèn)值為 View。此容器的 parentNodeScrollView,childNodes 是列表。

innerRef: Ref | Function

列表內(nèi)部容器組件的 ref。

layout: string = ‘vertical’

滾動(dòng)方向。vertical 為垂直滾動(dòng),horizontal 為平行滾動(dòng)。默認(rèn)為 vertical。

onScroll: Function

列表滾動(dòng)時(shí)調(diào)用函數(shù),函數(shù)的第一個(gè)參數(shù)為對(duì)象,由三個(gè)屬性構(gòu)成:

  • scrollDirection,滾動(dòng)方向,可能值為 forward 往前, backward 往后。
  • scrollOffset,滾動(dòng)距離
  • scrollUpdateWasRequested, 當(dāng)滾動(dòng)是由 scrollTo()scrollToItem() 調(diào)用時(shí)返回 true,否則返回 false

onScrollNative: Function

調(diào)用平臺(tái)原生的滾動(dòng)監(jiān)聽函數(shù)。

overscanCount: number = 1

在可視區(qū)域之外渲染的列表單項(xiàng)數(shù)量,值設(shè)置得越高,快速滾動(dòng)時(shí)出現(xiàn)白屏的概率就越小,相應(yīng)地,每次滾動(dòng)的性能會(huì)變得越差。

useIsScrolling: boolean

是否注入 isScrolling 屬性到 children 組件。這個(gè)參數(shù)一般用于實(shí)現(xiàn)滾動(dòng)骨架屏(或其它 placeholder) 時(shí)比較有用。

其它 ScrollView 組件的參數(shù)

除了以上參數(shù),所有 ScrollView 組件的參數(shù)都可以傳入 VirtualList 組件,沖突時(shí)優(yōu)先使用以上文檔描述的參數(shù)。

方法

通過 React.createRef() 創(chuàng)建 ref,掛載到 VirtualList 上可以訪問到 VirtualList 的內(nèi)部方法:

  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} /* 渲染列表數(shù)據(jù) */
  19. itemCount={dataLen} /* 渲染列表的長度 */
  20. itemSize={100} /* 列表單項(xiàng)的高度 */
  21. ref={this.list}
  22. >
  23. {Row} /* 列表單項(xiàng)組件,這里只能傳入一個(gè)組件 */
  24. </VirtualList>
  25. );
  26. }
  27. }

scrollTo(scrollOffset: number): void

滾動(dòng)到指定的地點(diǎn)。

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

滾動(dòng)到指定的條目。

第二參數(shù) align 的值可能為:

  • auto: 盡可能滾動(dòng)距離最小保證條目在可視區(qū)域中,如果已經(jīng)在可視區(qū)域,就不滾動(dòng)
  • smart: 條目如果已經(jīng)在可視區(qū)域,就不滾動(dòng);如果有部分在可視區(qū)域,盡可能滾動(dòng)距離最小保證條目在可視區(qū)域中;如果條目完全不在可視區(qū)域,那就滾動(dòng)到條目在可視區(qū)域居中顯示
  • center: 讓條目在可視區(qū)域居中顯示
  • end: 讓條目在可視區(qū)域末尾顯示
  • start: 讓條目在可視區(qū)域末尾顯示

Vue

在 Vue 中使用虛擬列表,我們需要在入口文件聲明使用:

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

一個(gè)最簡單的長列表組件會(huì)像這樣,virtual-list 的 5 個(gè)屬性都是必填項(xiàng):

  1. <! –– row.vue 單項(xiàng)組件 ––>
  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>

無限滾動(dòng)

實(shí)現(xiàn)無限滾動(dòng)也非常簡單,我們只需要在列表滾動(dòng)到底部時(shí),往列表尾部追加數(shù)據(jù)即可:

  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. // 避免重復(fù)加載數(shù)據(jù)
  49. !this.loading &&
  50. // 只有往前滾動(dòng)我們才觸發(fā)
  51. scrollDirection === 'forward' &&
  52. // 5 = (列表高度 / 單項(xiàng)列表高度)
  53. // 100 = 滾動(dòng)提前加載量,可根據(jù)樣式情況調(diào)整
  54. scrollOffset > ((this.dataLen - 5) * this.itemHeight + 100)
  55. ) {
  56. this.listReachBottom()
  57. }
  58. }
  59. }
  60. }
  61. </script>

props

item: VueComponent

將要渲染的列表單項(xiàng)組件。組件的 props 有 4 個(gè)屬性:

  • css: 單項(xiàng)的樣式,樣式必須傳入組件的 style
  • data: 組件渲染的數(shù)據(jù)
  • index: 組件渲染數(shù)據(jù)的索引
  • isScrolling: 組件是否正在滾動(dòng),當(dāng) useIsScrolling 值為 true 時(shí)返回布爾值,否則返回 undefined

itemCount: number

列表的長度。必填。

itemData: Array

渲染數(shù)據(jù)。必填。

itemSize: number

列表單項(xiàng)的大小,垂直滾動(dòng)時(shí)為高度,水平滾動(dòng)時(shí)為寬度。必填。

height: number | string

列表的高度。當(dāng)滾動(dòng)方向?yàn)榇怪睍r(shí)必填。

width: number | string

列表的寬度。當(dāng)滾動(dòng)方向?yàn)樗綍r(shí)必填。

wclass: string

根組件 CSS 類

wstyle: Style

根組件的樣式

initialScrollOffset: number = 0

初始滾動(dòng)偏移值,水平滾動(dòng)影響 scrollLeft,垂直滾動(dòng)影響 scrollTop

innerElementType: string = ‘view’

列表內(nèi)部容器組件類型,默認(rèn)值為 view。此容器的 parentNodescroll-view,childNodes 是列表。

layout: string = ‘vertical’

滾動(dòng)方向。vertical 為垂直滾動(dòng),horizontal 為平行滾動(dòng)。默認(rèn)為 vertical

v-on:scroll: Function

列表滾動(dòng)時(shí)調(diào)用函數(shù),函數(shù)的第一個(gè)參數(shù)為對(duì)象,由三個(gè)屬性構(gòu)成:

  • scrollDirection,滾動(dòng)方向,可能值為 forward 往前, backward 往后。
  • scrollOffset,滾動(dòng)距離
  • scrollUpdateWasRequested, 當(dāng)滾動(dòng)是由 scrollTo()scrollToItem() 調(diào)用時(shí)返回 true,否則返回 false

scrollNative: Function

調(diào)用平臺(tái)原生的滾動(dòng)監(jiān)聽函數(shù)。注意調(diào)用傳遞此函數(shù)時(shí)使用的是 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

在可視區(qū)域之外渲染的列表單項(xiàng)數(shù)量,值設(shè)置得越高,快速滾動(dòng)時(shí)出現(xiàn)白屏的概率就越小,相應(yīng)地,每次滾動(dòng)的性能會(huì)變得越差。

useIsScrolling: boolean

是否注入 isScrolling 屬性到 item 組件。這個(gè)參數(shù)一般用于實(shí)現(xiàn)滾動(dòng)骨架屏(或其它 placeholder) 時(shí)比較有用。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)