技术
vue、table
一、页面介绍
最近做个车型参数配置页,页面长下面这样,包括输入框,单选多选,下拉框,横向配置功能。
二、优化前效果演示以及分析问题
可以看的出来,页面异常卡顿,列头跟不上滑动,输入框卡的怀疑人生,做出来以后就开始分析问题,以及想对应的解决办法,当然最先怀疑的肯定是dom过多,但是看了汽车之家等类似页面,发现dom也是那么多,人家还是那么流畅,尝试解决问题:
- 首先就是优化后端返回来的数据,把所有无用的属性删除,想法很简单,以为能减轻vue监听的数据量,但是现实啪啪打脸,并没任何效果,思考了一下,vue框架应该不会监听view层没有用到的model,具体源码忘了,所以优化后端的数据就当个心里安慰吧。
- 其次就是把所有单元格的数据都写死,不再是根据类型各种判断显示出不同的输入框单选框什么的,嗯,这回速度唰唰的,基本猜到问题应该是两方面组成的,一个是template里的循环,条件判断太多,另一个是dom元素也多,第一种我没办法解决,因为这就是配置页面,不像汽车之家的展示页面,而且数据结构比较复杂,关联性比较强,循环起来比较多。所以自然想到优化dom元素在页面的数量这个办法了,下面开启讲解之旅。
三、从静态页面布局开始
拿到页面时想到的是elementui的表格有没有这种支持横向列的,呃,找半天没有,然后就默默的打开了汽车之家页面的开发者模式,借鉴了一下布局,是用纯table写的,我又根据我的需求改动了一些,但是大体跟汽车之家一样。
首先上面是一块,下面整体是一块,但是每个组又是一个table 大概就是这么个结构布局,下面是html结构代码,对应再写上css 静态页面就ok了。四、数据结构
数据结构这块可以看下图:
- 简单说下 config 是所有的参数,需要根据parent_cid把扁平化数组转成树结构的,代码稍后供上
- model 是车型数组,需要用config_list里的值 跟config里面key_value 对应上,表示的就是这个车型当前某个参数的值。
- config中有个value_area 以及 data_type,data_type是该参数的类型,比如单选多选下拉框输入框,value_area就是该参数值的范围。
- 所以我说循环判断的条件太多了,导致页面卡。
五、部分代码
- 数组转树代码
formatarr(config){
let parents = config.filter(value => value.parent_cid == 0 )
let children = config.filter(value => value.parent_cid !== 0 )
let translator = (parents, children) => {
parents.forEach((parent) => {
children.forEach((current, index) => {
if (current.parent_cid === parent.id) {
parent.sub !== undefined ? parent.sub.push(current) : parent.sub = [current]
let temp = JSON.parse(JSON.stringify(children))
temp.splice(index, 1)
translator([current], temp)
}
}
)
}
)
}
translator(parents, children)
return parents
}
- 下面是单个table的模版 ,一个table就是一组参数,可以略见数据的复杂,循环判断特别多。
<div class="conbox" style=" margin-top: 100px; overflow: auto; height: 500px;" @scroll="scrollevent" >
<div v-for="(item,index) in config" :key=item.id :style="{ minHeight : (item.sub && item.sub.length+1)*61+'px'}" :ref="item.id">
<table class="tbcs" v-if="index==0 || item.showdom" >
<tbody>
<tr>
<th class="cstitle" colspan="20">
<h3>
<span>{{item.label}}</span>
</h3>
</th>
</tr>
<tr style="background: rgb(255, 255, 255);" v-for="subItem in item.sub" :key=subItem.id>
<th>
<div style="
display: flex;
justify-content: space-between;
align-items: center;
width:169px" >
<span>{{subItem.label}}</span>
<el-button size="mini" style="margin-right: 10px;" @click="hengxiang(subItem.key_value)" >横向配置</el-button>
</div>
</th>
<td v-for="item in model" :key=item.id>
<div v-if=" judgeconfig_list( item['config_list'] )">
<div v-if="subItem['data_type'] == 'input' ">
<el-input v-model="item['config_list'][subItem.key_value]" :placeholder=subItem.label ></el-input>
</div>
<div v-else-if="subItem['data_type'] == 'select' ">
<el-select v-model="item['config_list'][subItem.key_value] " placeholder="请选择">
<el-option
v-for="item in JSON.parse(subItem['value_area']) "
:key="item.label"
:label="item.label"
:value="item.label"
>
</el-option>
</el-select>
</div>
<div v-else-if="subItem['data_type'] == 'radio' ">
<el-radio style="display: block; padding-top: 2px;" v-for="ritem in JSON.parse(subItem['value_area'])" :key="ritem.key"
v-model="item['config_list'][subItem.key_value]" :label="ritem.key + ','+ ritem.label + ','">
{{ritem.label}}
</el-radio>
</div>
<div v-else-if="subItem['data_type'] == 'checkbox' ">
<el-checkbox-group v-model="item['config_list'][subItem.key_value]">
<el-checkbox v-for="ritem in JSON.parse(subItem['value_area'])" :key="ritem.key" style="display: block;"
:label="ritem.key + ','+ ritem.label + ','" >
<span style="
height: 10px;
display: inline-block;
width: 10px;
background: black;
border-radius: 50px;" v-if="filtercheckbox(ritem.key) == 1" ></span>
<span style="
height: 8px;
display: inline-block;
width: 8px;
background: white;
border-radius: 50px;
border: 1px solid black;" v-else-if="filtercheckbox(ritem.key) == 2" ></span>
<span style="
height: 10px;
display: inline-block;
width: 10px;
" v-else> </span>
<span class="el-checkbox__label">{{ritem.label}}</span>
</el-checkbox>
</el-checkbox-group>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
- 滚动事件的代码
scrollevent(e){
this.scrollleft = -e.target.scrollLeft
let scrolltop = e.target.scrollTop
for (const key in this.$refs) {
if (this.$refs.hasOwnProperty(key)) {
const element = this.$refs[key];
if((650+scrolltop)>element[0].offsetTop ){
for(let i = 0;i<this.config.length;i++){
if (this.config[i].id == key){
this.config[i]['showdom'] =true
break;
}
}
}else{
for(let i = 0;i<this.config.length;i++){
if (this.config[i].id == key){
this.config[i]['showdom'] =false
break;
}
}
}
if( scrolltop > (element[0].offsetTop + element[0].offsetHeight)){
for(let i = 0;i<this.config.length;i++){
if (this.config[i].id == key){
this.config[i]['showdom'] =false
break;
}
}
}
}
}
}
- 主动执行的方法
async mounted() {
await this.getdata()
this.init()
},
init(){
for (const key in this.$refs) {
if (this.$refs.hasOwnProperty(key)) {
const element = this.$refs[key];
if( 650>element[0].offsetTop){
for(let i = 0;i<this.config.length;i++){
if (this.config[i].id == key){
this.config[i]['showdom'] =true
break;
}
}
}
}
}
六、开始讲解
- 首先,删除dom以及新增dom的思路跟懒加载类似,页面滚动到下面的div快露出来了,就把table插入到div里,这里很重要的一点就是div要占位,要有高度,但是这个高度怎么算的呢,因为这不是普通那种全是图片的结构,这是俩级结构,所以我只想按table来一个一个的展示,table里面的不做dom的删改。 table外面的div刚开始里面是空的,高度自行根据table里面有多少内容算出 minHeight : (item.sub && item.sub.length+1)*61+'px'
- 其次要在父节点有一个属性,标记当前表格显示隐藏的状态,这个需要在数据处理时加进去,默认全是false,然后在循环时 table 里做这个判断 v-if="index==0 || item.showdom" 第一位的展示或者showdom为true
- 最后就开始在滚动事件里做处理 重新优化下,原来的思路在删除dom时可能会由于数据的问题产生bug,重新整理了思路,展示dom正常还是那个判断条件:scrolltop+屏幕可视高度如果大于元素的offsetTop就把showdom = true。删除dom的思路要变一下,首先是删除可视区域上面dom的情况,scrolltop 如果大于元素的offsetTop+元素的offsetHeight说明当前的dom已经移动到可视区域外了,就可以删除了showdom = false。还有一个就是删除可视区域下面的dom,跟显示的条件相反就行。
- 记得要在数据回来后主动执行下对比的方法,以免数据的问题,第一个表格的数据没占满可视区域。
- 说下这种解决的优缺点吧,优点毋庸置疑,页面不卡了,缺点也显而易见,就是滚动快了会增加白屏时间,但是利大于弊,所以必须这么做。
七、总结
至此这篇文章就结束了,其中每个细节部分都不简单,都要仔细思考,要不我也不会写出来, 如果还有什么问题评论区留言,我尽量解答。希望看完不要忘记点赞,手敲不易。
八、补充
能不用table布局就不用了吧,可以使用display属性模拟table。测试了下,如果横向数据十多条是没什么问题,但是几十条的话,table会比div卡很多。