某天闲着无聊想练一下手速,去上拉一个小程序项目中一个有1万多条商品数据的列表。在数据加载到1000多条后,是列表居然出现了白屏。看了一下控制台:
‘dom limit exceeded’,dom数超出了限制, 不知道微信是出于什么考虑,要限制页面的dom数量。
写了个小dome做了个测试。 listData的数据结构为:
listData:[
{
isDisplay:true,
itemList:[{
qus:'下面哪位是刘发财女朋友?',
answerA:'刘亦菲',
answerB:'迪丽热巴',
answerC:'斋藤飞鸟',
answerD:'花泽香菜',
}
.......//20条数据
]
}]
页面渲染效果:
<view wx:for="{{listData}}" class="first-item" wx:for-index="i" wx:for-item="firstItem" wx:key="i" wx:if="{{firstItem.isDisplay}}">
<view class="item-list" wx:for="{{firstItem.itemList}}" wx:key="index">
<view>{{item.qus}}</view>
<view class="answer-list">
<view>A. <text>{{item.answerA}}</text></view>
<view>B. <text>{{item.answerB}}</text></view>
<view>C. <text>{{item.answerC}}</text></view>
<view>D. <text>{{item.answerD}}</text></view>
</view>
</view>
</view>
<view wx:for="{{listData}}" class="first-item" wx:for-index="i" wx:for-item="firstItem" wx:key="i" wx:if="{{firstItem.isDisplay}}">
<view class="item-list" wx:for="{{firstItem.itemList}}" wx:key="index">
<view>{{item.qus}}</view>
<view class="answer-list">
<view>A. {{item.answerA}}</view>
<view>B. {{item.answerB}}</view>
<view>C. {{item.answerC}}</view>
<view>D. {{item.answerD}}</view>
</view>
</view>
</view>
通过大致计算,一个小程序页面大概可以渲染2万个wxml节点 而小程序官方的性能测评得分条件为少于1000个wxml节点官方链接
setData接口的调用涉及逻辑层与渲染层间的线程通信,通信过于频繁可能导致处理队列阻塞,界面渲染不及时而导致卡顿,应避免无用的频繁调用。
得分条件:每秒调用setData的次数不超过 20 次
由于小程序运行逻辑线程与渲染线程之上,setData的调用会把数据从逻辑层传到渲染层,数据太大会增加通信时间。
得分条件:setData的数据在JSON.stringify后不超过 256KB
建议一个页面使用少于 1000 个 WXML 节点,节点树深度少于 30 层,子节点数不大于 60 个。一个太大的 WXML 节点树会增加内存的使用,样式重排时间也会更长,影响体验。
得分条件:页面WXML节点少于 1000 个,节点树深度少于 30 层,子节点数不大于 60 个
由上面的测试dome可知,在不影响代码运行和可读性的前提下,尽量减少标签的嵌套,可以大幅的增加页面数据的列表条数,毕竟公司不是按代码行数发工资的。如果你的列表数据量有限,可以用这种方法来增加列表渲染条数。如果数据量很大,再怎么精简也超过2万的节点,这个方法则不适用。
如图五所示,小程序setDate的性能会受到setData数据量大小和调用频率限制。所以要围绕减少每一次setData数据量大小,降低setData调用频率进行优化。 #####(1)删除冗余字段 后端的同事经常把数据从数据库中取出就直接返回给前端,不经过任何处理,所以会导致数据大量的冗余,很多字段根本用不到,我们需要把这些字段删除,减少setDate的数据大小。 #####(2)setData的进阶用法 通常,我们对data中数据的增删改操作,是把原来的数据取出,处理,然后用setData整体去更新,比如我们列表中使用到的上拉加载更多,需要往listData尾部添加数据:
newList=[{...},{...}];
this.setData({
listData:[...this.data.listData,...newList]
})
这样会导致 setDate的数据量越来越大,页面也越来越卡。
setDate的正确使用姿势
比如我们要修改数组listData第一个元素的isDisplay属性,我们可以这样操作:
let index=0;
this.setData({
[`listData[${index}].isDisplay`]:false,
})
如果我们想同时修改数组listData中下标从0到9的元素的isDisplay属性,那要如何处理呢?你可能会想到用for循环来执行setData:
for(let index=0;index<10;index++){
this.setData({
[`listData[${index}].isDisplay`]:false,
})
}
那么这样就会导致另外一个问题,那就是listData的调用过于频繁,也会导致性能问题,正确的处理方式是先把要修改的数据先收集起来,然后调用setData一次处理完成:
let changeData={};
for(let index=0;index<10;index++){
changeData[[`listData[${index}].isDisplay`]]=false;
}
this.setData(changeData);
这样我们就把数组listData中下标从0到9的元素的isDisplay属性改成了false。
如果只添加一条数据
let newData={...};
this.setData({
[`listData[${this.data.listData.length}]`]:newData
})
如果是添加多条数据
let newData=[{...},{...},{...},{...},{...},{...}];
let changeData={};
let index=this.data.listData.length
newData.forEach((item) => {
changeData['listData[' + (index++) + ']'] = item //赋值,索引递增
})
this.setData(changeData)
至于删除操作,还没有找到更好的方法,不知道大家有什么方法可以分享吗?
可以把列表的一行或者多行封装到自定义组件里,在列表页使用一个组件,只算一个节点,这样你的列表能渲染的数据可以成倍数的增加。组件内的节点数也是有限制的,但是你可以一层层嵌套组件实现列表的无限加载,如果你不怕麻烦的话
经过上面的一系列操作后,列表的性能会得到很大的提升,但是如果数据量实在太大,wxml节点数也会超出限制,导致页面发生错误。我们的处理方法是使用虚拟列表,页面只渲染当前可视区域以及可视区域上下若干条数据的节点,通过isDisplay控制节点的渲染。
使用二维数组,因为如果是一维数组,页面滚动需要用setData设置大量的元素isDispaly属性来控制列表的的渲染。而二维数组可以这可以一次调用setData控制十条,二十条甚至更多的数据的渲染。
listData:[
{
isDisplay:true,
itemList:[{
qus:'下面哪位是刘发财女朋友?',
answerA:'刘亦菲',
answerB:'迪丽热巴',
answerC:'斋藤飞鸟',
answerD:'花泽香菜',
}
.......//二维数组中的条数根据项目实际情况
]
}]
data{
itemHeight:4520,//列表第一层dom高度,单位为rpx
itemPxHeight:'',//转化为px高度,因为小程序获取的滚动条高度单位为px
aboveShowIndex:0,//已渲染数据的第一条的Index
belowShowNum:0,//显示区域下方隐藏的条数
oldSrollTop:0,//记录上一次滚动的滚动条高度,判断滚动方向
prepareNum:5,//可视区域上下方要渲染的数量
throttleTime:200,//滚动事件节流的时间,单位ms
}
<!-- above区域的 -->
<view class="above-box" style="height:{{aboveShowIndex*itemHeight}}rpx"> </view>
<!-- 实际渲染的区域的 -->
<view wx:for="{{listData}}" class="first-item" wx:for-index="i" wx:for-item="firstItem" wx:key="i" wx:if="{{firstItem.isDisplay}}">
<view class="item-list" wx:for="{{firstItem.itemList}}" wx:key="index">
<view>{{item.qus}}</view>
<view class="answer-list">
<view>A. {{item.answerA}}</view>
<view>B. {{item.answerB}}</view>
<view>C. {{item.answerC}}</view>
<view>D. {{item.answerD}}</view>
</view>
</view>
</view>
<!-- below区域的 -->
<view class="below-box" style="height:{{belowShowNum*itemHeight}}rpx"> </view>
let query = wx.createSelectorQuery();
query.select('.content').boundingClientRect(rect=>{
let clientWidth = rect.width;
let ratio = 750 / clientWidth;
this.setData({
itemPxHeight:Math.floor(this.data.itemHeight/ratio),
})
}).exec();
function throttle(fn){
let valid = true
return function() {
if(!valid){
return false
}
// 工作时间,执行函数并且在间隔期内把状态位设为无效
valid = false
setTimeout(() => {
fn.call(this,arguments);
valid = true;
}, this.data.throttleTime)
}
}
onPageScroll:throttle(function(e){
let scrollTop=e[0].scrollTop;//滚动条高度
let itemNum=Math.floor(scrollTop/this.data.itemPxHeight);//计算出可视区域的数据Index
let clearindex=itemNum-this.data.prepareNum+1;//滑动后需要渲染数据第一条的index
let oldSrollTop=this.data.oldSrollTop;//滚动前的scrotop,用于判断滚动的方向
let aboveShowIndex=this.data.aboveShowIndex;//获取已渲染数据第一条的index
let listDataLen=this.data.listData.length;
let changeData={}
//向下滚动
if(scrollTop-oldSrollTop>0){
if(clearindex>0){
//滚动后需要变更的条数
for(let i=aboveShowIndex;i<clearindex;i++){
changeData[[`listData[${i}].isDisplay`]]=false;
let belowShowIndex=i+2*this.data.prepareNum;
if(i+2*this.data.prepareNum<listDataLen){
changeData[[`listData[${belowShowIndex}].isDisplay`]]=true;
}
}
}
}else{//向上滚动
if(clearindex>=0){
let changeData={}
for(let i=aboveShowIndex-1;i>=clearindex;i--){
let belowShowIndex=i+2*this.data.prepareNum
if(i+2*this.data.prepareNum<=listDataLen-1){
changeData[[`listData[${belowShowIndex}].isDisplay`]]=false;
}
changeData[[`listData[${i}].isDisplay`]]=true;
}
}else{
if(aboveShowIndex>0){
for(let i=0;i<aboveShowIndex;i++){
this.setData({
[`listData[${i}].isDisplay`]:true,
})
}
}
}
}
clearindex=clearindex>0?clearindex:0
if(clearindex>=0&&!(clearindex>0&&clearindex==this.data.aboveShowIndex)){
changeData.aboveShowIndex=clearindex;
let belowShowNum=this.data.listData.length-(2*this.data.prepareNum+clearindex)
belowShowNum=belowShowNum>0?belowShowNum:0
if(belowShowNum>=0){
changeData.belowShowNum=belowShowNum
}
this.setData(changeData)
}
this.setData({
oldSrollTop:scrollTop
})
}),
经过上面的处理后,页面的wxml节点数量相对稳定,可能因为可视区域数据的index计算误差,页面渲染的数据有小幅度的浮动,但是已经完全不会超过小程序页面的节点数量的限制。理论上100万条数据的列表也不会有问题,只要你有耐心和精力一直划列表加载这么多数据。
虽然不知道为什么,但是直觉告诉我使用自定义组件性能会相对差一点。为了对比两种方法的优劣,使用了Trace工具对一个5000条带图片数据进行了性能测试。
内存占用对比:
自定义组件内存占用情况:
虚拟列表内存占用情况:
对比可以看出,因为组件在上拉加载时,组件是没有销毁的,导致数据量逐渐增多。而虚拟列表在增加数据的同时,也会销毁相同数量的数据,所以内存占比会稳定在一个数量。具体到这个测试dome,5000条数据使用自定义组件,最后占用2000MB的内存,而虚拟列表稳定在700MB。
setData后重新渲染所用的时间对比:
自定义组件重新渲染耗时:
虚拟列表重新渲染耗时:
从测试结果可以看出,无论是耗时的次数分布,还是最大耗时,最小耗时,虚拟列表都优于自定义组件
最后附上虚拟列表的github地址,如果对您有帮助,记得给个小星星哦
作者:刘发福
链接:https://juejin.cn/post/6966904317148299271
来源:掘金
使用外层容器切割:给每一个 li 设定右边框和下边框线,使用CSS选择器此方法仅适用于每行固定显示两个li的情况,不需要计算宽高,也不需要设置父容器。使用table通过CSS选择器li:nth-last-child(2)和li:last-child隐藏最后两个li的下边框
无序列表使用标签:<ul>,<li>属性:disc,circle,square;有序列表使用标签:<ol>,<li>属性:A,a,I,i,start;嵌套列表使用标签:<ul>,<ol>,<li> ;自定义列表使用标签:<dl>,<dt>,<dd>
实际上用了overflow:hidden 会影响 list-style,即当ul 中的li 的overflow 为hidden的时候, list-style不起作用,不显示前面的点、圈等样式。
在我们平时的业务开发中经常会用到文案超出只有收起,点击在展示全部文案;通常的使用时使用css来实现
多选列表 (Multi-Select) 是一种将所有选项列出,并允许用户利用 Ctrl/Shift 键进行多选的 UI 元素。这是一种常见的设计元素。有时候为了节省空间,我们会将选项折叠于 Combo Box 中
比如 umy-ui (ux-table)虚拟列表table组件, vue-virtual-scroller 以及 react-virtualized 这些优秀的插件快速满足业务需要。为了理解插件背后的原理机制,我们实现一个自己简易版的虚拟列表,希望在实际业务项目中能带来一些思考和帮助。
常用写法是用 Arrary.prototype.map 方法,将数组形式的数据映射为 JSX.Element 数组,并嵌入到组件要返回的 JSX.Element 中,如下:
在正文之前,先对虚拟列表做个简单的定义。根据上文,虚拟列表是按需显示思路的一种实现,即虚拟列表是一种根据滚动容器元素的可视区域来渲染长列表数据中某一个部分数据的技术。
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!