list教程

了解如何正确使用list,优化列表渲染性能,灵活实现需求

通过本节,你将学会:

  • 适用场景
  • 性能优化
  • 效果展示:吸顶

适用场景

简单场景

开发者在页面中实现长列表或者屏幕滚动等效果时,习惯使用div组件做循环遍历

示例如下:

假设开发者要这样的效果:一个结构简单的商品列表

使用div组件的代码如下:

<template>
  <!-- div实现 -->
  <div class="tutorial-page">
    <!-- 商品列表 -->
    <block for="productList">
      <div class="content-item" onclick="route($item.url)">
        <image class="img" src="{{$item.img}}"></image>
        <div class="text-wrap">
          <div class="top-line">
            <text class="text-name">{{$item.name}}</text>
            <text class="text-price">{{$item.price}}</text>
          </div>
          <text class="bottom-line">{{$item.brief}}</text>
        </div>
      </div>
    </block>

    <!-- 加载更多,监听通用事件appear,出现时加载更多数据 -->
    <div class="load-more" onappear="loadMoreData">
      <progress type="circular"></progress>
      <text>加载更多</text>
    </div>
  </div>
</template>

然而,当DOM结构复杂时,滚动页面会出现卡顿现象,因为Native无法复用div组件实现的列表元素

为了得到流畅的列表滚动体验,推荐开发者使用list组件替代div组件实现长列表布局,因为Native会复用相同type属性list-item

使用list组件的代码如下:

<template>
  <!-- 列表实现 -->
  <list class="tutorial-page" onscrollbottom="loadMoreData">
    <!-- 商品列表 -->
    <block for="productList">
      <list-item type="product" class="content-item" onclick="route($item.url)">
        <image class="img" src="{{$item.img}}"></image>
        <div class="text-wrap">
          <div class="top-line">
            <text class="text-name">{{$item.name}}</text>
            <text class="text-price">{{$item.price}}</text>
          </div>
          <text class="bottom-line">{{$item.brief}}</text>
        </div>
      </list-item>
    </block>

    <!-- 加载更多,type属性自定义命名为loadMore -->
    <list-item type="loadMore" class="load-more">
      <progress type="circular"></progress>
      <text>加载更多</text>
    </list-item>
  </list>
</template>

要实现DOM片段的复用,要求相同type属性的DOM结构完全相同。所以,设置相同type属性list-item是优化列表滚动性能的关键

注意:

  • list-item内不能再嵌套list
  • list-itemtype属性为必填属性
  • list-item内部需谨慎使用if指令for指令,因为相同type属性list-item的DOM结构必须完全相同,而使用if指令for指令会造成DOM结构差异

提示:

若遇到类似xxx cannot be cast to xxx at ...list的错误,请检查list-item组件是否存在如下情形:

  • 未设置type属性。解决方案:设置type属性
  • 内部使用了if指令。解决方案:使用show指令代替if指令,或设置不同的type属性
  • 设置为相同的type属性,但DOM结构不一致。解决方案:设置不同的type属性

复杂场景

实现简单的商品列表,了解list组件的基本用法和优化性能的关键后,接下来通过实现多种列表元素类型的复杂列表,进一步了解list组件

示例如下:

假设开发者要实现这样的效果:一个商品列表页,图片位于左边和图片位于右边的商品交错显示

列表中的列表元素可以分为三类,设置三种不同type属性list-item。分别为:

  • 图片在左,文字在右的list-itemtype属性自定义命名为productLeft
  • 图片在右,文字在左的list-itemtype属性自定义命名为productRight
  • 加载更多的list-itemtype属性自定义命名为loadMore

示例代码如下:

<template>
  <!-- list中可以划分为三种类型的DOM结构,对应三种type属性的list-item -->
  <list class="tutorial-page" onscrollbottom="loadMoreData">
    <block for="{{productList}}">
      <!-- 图片在左,文字在右的list-item,type属性自定义命名为productLeft -->
      <list-item type="productLeft" class="content-item" if="{{$idx%2 === 0}}" onclick="route($item.url)">
        <image class="img" src="{{$item.img}}"></image>
        <div class="text-wrap">
          <div class="top-line">
            <text class="text-name">{{$item.name}}</text>
            <text class="text-price">{{$item.price}}</text>
          </div>
          <text class="bottom-line">{{$item.brief}}</text>
        </div>
      </list-item>

      <!-- 图片在右,文字在左的list-item,type属性自定义命名为productRight -->
      <list-item type="productRight" class="content-item" if="{{$idx%2 === 1}}" onclick="route($item.url)">
        <div class="text-wrap">
          <div class="top-line">
            <text class="text-name">{{$item.name}}</text>
            <text class="text-price">{{$item.price}}</text>
          </div>
          <text class="bottom-line">{{$item.brief}}</text>
        </div>
        <image class="img" src="{{$item.img}}"></image>
      </list-item>
    </block>

    <!-- 加载更多的list-item,type属性自定义命名为loadMore -->
    <list-item type="loadMore" class="load-more">
      <progress type="circular"></progress>
      <text>加载更多</text>
    </list-item>
  </list>
</template>

性能优化

当DOM结构复杂时,为了得到流畅的列表滚动体验,list组件的性能优化必不可缺

list组件的性能优化分为精简DOM层级复用list-item细粒度划分list-item关闭scrollpage四个方面

其中,精简DOM层级复用list-item是使用list组件必须遵循的优化原则,细粒度划分list-item关闭scrollpage适用于部分场景,详见下文

精简DOM层级

精简DOM层级,即减少DOM树的级数和分支上的DOM节点数。层级越少、数量越少,布局和绘制就会越快

因此,开发者需要尽量剔除list中无意义的包裹类标签和层级

复用list-item

复用list-item,即列表中相同的DOM结构设置为同一type属性list-item,这是优化列表滚动体验的关键

细粒度划分list-item

细粒度划分list-item,即列表中相同的DOM结构划分为尽可能小的列表元素(即list-item

示例如下:

假设开发者要实现这样的效果:商品按类别分类,展示多种类别

从业务角度,可按类别划分为不同type属性list-item

然而,当list-item复杂时,会出现卡顿现象。推荐抛开业务逻辑,划分为尽可能小的列表元素

示例代码如下:

<template>
  <list class="tutorial-page" onscrollbottom="loadMoreData">
    <!-- 细粒度划分list-item -->
    <block for="productList">
      <!-- title -->
      <list-item type="title" if="$item.title" class="title {{$idx>0?'margin-top':''}}">
        <text>{{$item.title}}</text>
      </list-item>
      <!-- banner -->
      <list-item type="banner" if="$item.bannerImg" class="banner">
        <image src="{{$item.bannerImg}}"></image>
      </list-item>
      <!-- productMini -->
      <list-item type="{{'productMini'+$item.productMini.length}}" if="$item.productMini" class="product-mini-wrap">
        <!-- 在当前list-item中使用了for指令,因此需要动态设置list-item的type属性。确保相同type属性的list-item的DOM结构完全一致 -->
        <div for="value in $item.productMini" class="product-mini">
          <image src="{{value.img}}" class="product-mini-img"></image>
          <text>{{value.name}}</text>
          <text class="product-mini-brief">{{value.brief}}</text>
          <text class="product-mini-price">{{value.price}}</text>
        </div>
      </list-item>
      <!-- textHint -->
      <list-item type="textHint" if="$item.textHint" class="text-hint">
        <text>{{$item.textHint}} ></text>
      </list-item>
    </block>
    <!-- list底部的加载更多 -->
    <list-item type="loadMore" class="load-more">
      <progress type="circular"></progress>
      <text>加载更多</text>
    </list-item>
  </list>
</template>

关闭scrollpage

list组件支持属性scrollpage,默认关闭,标志是否将顶部页面中非list的元素随list一起滚动。开启scrollpage会降低list渲染性能

因此,在开发者开启scrollpage前,推荐先尝试将顶部页面中非list的元素,作为一种或多种type属性list-item,移入list中,从而达到关闭scrollpage提高渲染性能的目的

示例如下:

假设开发者要实现这样的效果:顶部banner,banner下方为常见列表,需要整屏滚动

开发者一般会将页面划分为banner和list两部分,然后开启listscrollpage属性,实现整屏滚动

然而,开启scrollpage会降低list渲染性能,推荐将顶部banner作为一种特殊type属性list-item,移入list中,关闭scrollpage

示例代码如下:

<template>
  <!-- 列表实现,监听列表的scrollbottom事件,列表滚动到底部时加载更多数据 -->
  <list class="tutorial-page" onscrollbottom="loadMoreData">
    <list-item type="banner" class="banner">
      <image src="../../Common/img/demo_large.png"></image>
    </list-item>

    <!-- 商品列表 -->
    <block for="productList">
      <list-item type="product" class="content-item" onclick="route($item.url)">
        <image class="img" src="{{$item.img}}"></image>
        <div class="text-wrap">
          <div class="top-line">
            <text class="text-name">{{$item.name}}</text>
            <text class="text-price">{{$item.price}}</text>
          </div>
          <text class="bottom-line">{{$item.brief}}</text>
        </div>
      </list-item>
    </block>

    <!-- list-item实现的加载更多,type属性自定义命名为loadMore -->
    <list-item type="loadMore" class="load-more">
      <progress type="circular"></progress>
      <text>加载更多</text>
    </list-item>
  </list>
</template>

list-item懒加载

懒加载,简称lazyload,本质上是按需加载

在传统的页面中,常用lazyload优化网页的性能:

  • 实现:不加载全部页面资源,当资源即将呈现在浏览器可视区域时,再加载资源
  • 优点:加快渲染的同时避免流量浪费

在框架中,开发者也可使用lazyload概念优化列表的渲染:

  • 实现:提前fetch请求足够的列表数据保存在内存变量memList中,当list滚动到底部时,从memList中提取部分数据来渲染list-item。当memList中数据不足时,提前fetch请求数据,填充memList
  • 优点:每次网络请求与页面渲染的数据量不一致,减少首屏渲染占用的JS执行时间,减少渲染后续list-item的等待时间

示例如下:

假设开发者要实现这样的效果:一个商品列表,每次渲染10个商品

  • 渲染首屏时,请求数据保存在内存变量memList中,从memList中提取部分数据渲染列表
  • 加载更多时,首先检查memList中是否有足够数据,有则直接从memList中提取部分数据渲染,而不是直接进行网络请求,减少时间消耗。当memList中数据不足时,提前请求数据

示例代码如下:

<template>
  <!-- 列表实现,监听列表的scrollbottom事件,列表滚动到底部时加载更多数据 -->
  <list class="tutorial-page" onscrollbottom="renderMoreListItem">
    <!-- 商品列表 -->
    <block for="productList">
      <list-item type="product" class="content-item">
        <image class="img" src="{{$item.img}}"></image>
        <div class="text-wrap">
          <div class="top-line">
            <text class="text-name">{{$item.name}}</text>
            <text class="text-price">{{$item.price}}</text>
          </div>
          <text class="bottom-line">{{$item.brief}}</text>
        </div>
      </list-item>
    </block>

    <list-item type="loadStatus" class="load-status">
      <progress type="circular" show="{{hasMoreData}}"></progress>
      <text show="{{hasMoreData}}">加载更多</text>
      <text show="{{!hasMoreData}}">没有更多了~</text>
    </list-item>
  </list>
</template>

<script>
  import {dataComponentListLazyload} from '../../Common/js/data'

  // 模拟fetch请求数据
  function callFetch (callback) {
    setTimeout(function () {
      callback(dataComponentListLazyload)
    }, 500)
  }

  // 内存中存储的列表数据
  let memList = []

  export default {
    private: {
      productList: [],
      hasMoreData: true,
      // 每次渲染的商品数
      size: 10,
      // 是否正在fetch请求数据
      isLoadingData: false
    },
    onInit () {
      this.$page.setTitleBar({ text: 'list-item懒加载' })
      // 获取数据并渲染列表
      this.loadAndRender()
    },
    /**
     * 请求并渲染
     */
    loadAndRender (doRender = true) {
      this.isLoadingData = true
      // 重新请求数据并根据模式判断是否需要渲染列表
      callFetch(function (resList) {
        this.isLoadingData = false
        if (!resList) {
          console.error(`数据请求错误`)
        }
        else if (!resList.length) {
          this.hasMoreData = false
        }
        else {
          memList = memList.concat(resList)
          if (doRender) {
            this._renderList()
          }
        }
      }.bind(this))
    },
    _renderList () {
      // 渲染列表
      if (memList.length > 0) {
        const list = memList.splice(0, this.size)
        this.productList = this.productList.concat(list)
      }
      if (memList.length <= this.size) {
        // 提前请求新的数据
        this.loadAndRender(false)
      }
    },
    /**
     * 滑动到底部时加载更多
     */
    renderMoreListItem () {
      if (!this.isLoadingData) {
        this._renderList()
      }
    }
  }
</script>

注意:避免在ViewModel的数据属性中定义memList。因为在ViewModel的数据属性中定义变量会触发set/get数据驱动定义,而memList作为暂时保存数据的变量,不需监听数据变化

效果展示:吸顶

本部分非必读,旨在为有以下需求之一的开发者提供参考:

  • 需要判断页面滚动位置
  • 需要了解appear事件disappear事件

传统页面的实现思路

吸顶是传统web页面中的一种比较老的交互方式:

  • 吸顶元素的初始位置一般靠近页面顶部,但与顶部有一定的距离
  • 当手指向上滑动超过吸顶元素的初始位置时,把吸顶元素固定在顶部
  • 当手指向下滑动到达吸顶元素的初始位置时,取消吸顶元素在顶部的固定

吸顶在传统web页面中的实现思路是监听scroll事件,当页面滚动到一定位置时,做一些事情来改变吸顶元素在窗口中的位置

框架的实现思路

然而,与传统web页面不同,在框架中,scroll事件仅适用于list组件,且获取的值是滚动的相对坐标值,在使用时,需要通过累加来获取当前滚动位置的绝对坐标

此外,scroll事件在列表滚动时会被高频触发,存在潜在性能问题

因此,在框架中,推荐开发者使用appear事件disappear事件来实现吸顶效果,appear事件在组件出现时触发,disappear事件在组件消失时触发

appear事件disappear事件是组件的通用事件,文档中标有支持通用事件的组件都支持这两个事件,包括div组件list-item组件

灵活使用appear事件disappear事件,能实现大部分需要判断滚动位置的需求

框架的具体实现与代码

接下来,对应在list组件中实现吸顶效果的示例代码,具体分析实现思路

首先,了解顶部元素吸顶元素

  • 列表中的顶部元素type属性toplist-item
  • 列表中的吸顶元素type属性ceilinglist-item

然后,分析吸顶效果实现方案:

  • 使用stack组件做为整个页面的容器,stack组件的特性为:每个直接子组件按照先后顺序依次堆叠,覆盖前一个子组件
  • stack组件中增加一个排在最后的子组件,作为mask遮挡之前的子组件,显示效果为一直固定在顶部,这个mask吸顶元素渲染效果完全一致
  • 吸顶元素需要吸顶时,显示对应的mask,实现吸顶的效果;当吸顶元素不需要吸顶时,隐藏对应的mask

最后,判断吸顶条件:

  • 当页面向下滚动到顶部元素消失在视野时,吸顶元素需要固定在顶部,因此,监听顶部元素disappear事件,显示mask
  • 当页面向上滚动到顶部元素出现在视野时,吸顶元素需要取消固定,因此,监听顶部元素appear事件,隐藏mask

示例代码如下:

<template>
  <!-- 利用stack组件,使"列表中的吸顶元素对应的Mask"覆盖列表 -->
  <stack class="tutorial-page">
    <list class="list">
      <!-- 通过监听"列表中的顶部元素"的元素的appear和disappear事件,控制"列表中的吸顶元素对应的Mask"的显示 -->
      <list-item type="top" ondisappear="showMask" onappear="hideMask">
        <div class="height-300 bg-blue">
          <text>列表中的顶部元素</text>
        </div>
      </list-item>
      <!-- 列表中的吸顶元素 -->
      <list-item type="ceiling">
        <div class="height-300 bg-red">
          <text>列表中的吸顶元素</text>
        </div>
      </list-item>
      <!-- 普通列表元素 -->
      <list-item for="list" type="common" class="list-item">
        <text class="text">{{$item}}</text>
      </list-item>
    </list>

    <!-- 列表中的吸顶元素对应的Mask -->
    <div show="{{maskShow}}">
      <div class="height-300 bg-red">
        <text>列表中的吸顶元素</text>
      </div>
    </div>
  </stack>
</template>

<style lang="less">
  .tutorial-page {
    flex-direction: column;
    .list {
      width: 750px;
      flex-grow: 1;
      .list-item {
        height: 150px;
        border-bottom-width: 1px;
        border-bottom-color: #0faeff;
        .text {
          flex: 1;
          text-align: center;
        }
      }
    }
    .height-300 {
      height: 300px;
    }
    .bg-red {
      flex-grow: 1;
      justify-content: center;
      background-color: #f76160;
    }
    .bg-blue {
      flex-grow: 1;
      justify-content: center;
      background-color: #0faeff;
    }
  }
</style>

<script>
  export default {
    private: {
      maskShow: false,
      appearCount: 0,
      list: ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N']
    },
    onInit(){
      this.$page.setTitleBar({ text: '效果展示:吸顶' })
    },
    showMask () {
      this.maskShow = true
    },
    hideMask () {
      // 加载页面时,所有元素的appear事件都会被触发一次。因此,需要过滤第一次的appear事件
      if (this.appearCount) {
        this.maskShow = false
      } else {
        ++this.appearCount
      }
    }
  }
</script>

总结

了解list组件的特点,可以更好的提升页面性能,避免后期开发过程中引起的性能问题

条匹配 "" 的结果

    没有搜索到与 "" 相关的内容