了解如何正确使用 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内不能再嵌套listlist-item的type属性为必填属性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-item,type属性自定义命名为productLeftlist-item,type属性自定义命名为productRightlist-item,type属性自定义命名为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 节点数。层级越少、数量越少,布局和绘制就会越快
因此,开发者需要尽量剔除 list 中无意义的包裹类标签和层级
复用list-item,即列表中相同的 DOM 结构设置为同一type属性的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>
list组件支持属性scrollpage,默认关闭,标志是否将顶部页面中非list的元素随list一起滚动。开启scrollpage会降低list渲染性能
因此,在开发者开启scrollpage前,推荐先尝试将顶部页面中非list的元素,作为一种或多种type属性的list-item,移入list中,从而达到关闭scrollpage提高渲染性能的目的
示例如下:
假设开发者要实现这样的效果:顶部 banner,banner 下方为常见列表,需要整屏滚动
开发者一般会将页面划分为 banner 和 list 两部分,然后开启list的scrollpage属性,实现整屏滚动
然而,开启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>
懒加载,简称lazyload,本质上是按需加载
在传统的页面中,常用lazyload优化网页的性能:
可视区域时,再加载资源在框架中,开发者也可使用lazyload概念优化列表的渲染:
memList中,当list滚动到底部时,从memList中提取部分数据来渲染list-item。当memList中数据不足时,提前 fetch 请求数据,填充memListlist-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属性为top的list-item吸顶元素:type属性为ceiling的list-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 组件的特点,可以更好的提升页面性能,避免后期开发过程中引起的性能问题