了解如何正确使用 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-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属性
自定义命名为productLeft
list-item
,type属性
自定义命名为productRight
list-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 请求数据,填充memList
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属性
为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 组件的特点,可以更好的提升页面性能,避免后期开发过程中引起的性能问题