车型参数配置页的实现与优化(dom过多造成页面卡顿,优化dom)

2,402 阅读6分钟

技术

vue、table

一、页面介绍

最近做个车型参数配置页,页面长下面这样,包括输入框,单选多选,下拉框,横向配置功能。

二、优化前效果演示以及分析问题

可以看的出来,页面异常卡顿,列头跟不上滑动,输入框卡的怀疑人生,做出来以后就开始分析问题,以及想对应的解决办法,当然最先怀疑的肯定是dom过多,但是看了汽车之家等类似页面,发现dom也是那么多,人家还是那么流畅,尝试解决问题:

  1. 首先就是优化后端返回来的数据,把所有无用的属性删除,想法很简单,以为能减轻vue监听的数据量,但是现实啪啪打脸,并没任何效果,思考了一下,vue框架应该不会监听view层没有用到的model,具体源码忘了,所以优化后端的数据就当个心里安慰吧。
  2. 其次就是把所有单元格的数据都写死,不再是根据类型各种判断显示出不同的输入框单选框什么的,嗯,这回速度唰唰的,基本猜到问题应该是两方面组成的,一个是template里的循环,条件判断太多,另一个是dom元素也多,第一种我没办法解决,因为这就是配置页面,不像汽车之家的展示页面,而且数据结构比较复杂,关联性比较强,循环起来比较多。所以自然想到优化dom元素在页面的数量这个办法了,下面开启讲解之旅。

三、从静态页面布局开始

拿到页面时想到的是elementui的表格有没有这种支持横向列的,呃,找半天没有,然后就默默的打开了汽车之家页面的开发者模式,借鉴了一下布局,是用纯table写的,我又根据我的需求改动了一些,但是大体跟汽车之家一样。

首先上面是一块,下面整体是一块,但是每个组又是一个table 大概就是这么个结构布局,下面是html结构代码,对应再写上css 静态页面就ok了。

四、数据结构

数据结构这块可以看下图:

  1. 简单说下 config 是所有的参数,需要根据parent_cid把扁平化数组转成树结构的,代码稍后供上
  2. model 是车型数组,需要用config_list里的值 跟config里面key_value 对应上,表示的就是这个车型当前某个参数的值。
  3. config中有个value_area 以及 data_type,data_type是该参数的类型,比如单选多选下拉框输入框,value_area就是该参数值的范围。
  4. 所以我说循环判断的条件太多了,导致页面卡。

五、部分代码

  1. 数组转树代码
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
    }
  1. 下面是单个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>
  1. 滚动事件的代码
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;
             }
           }
         }
       }
     }
     
   }
  1. 主动执行的方法
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;
             }
           }
           
         }
       }
     }

六、开始讲解

  1. 首先,删除dom以及新增dom的思路跟懒加载类似,页面滚动到下面的div快露出来了,就把table插入到div里,这里很重要的一点就是div要占位,要有高度,但是这个高度怎么算的呢,因为这不是普通那种全是图片的结构,这是俩级结构,所以我只想按table来一个一个的展示,table里面的不做dom的删改。 table外面的div刚开始里面是空的,高度自行根据table里面有多少内容算出 minHeight : (item.sub && item.sub.length+1)*61+'px'
  2. 其次要在父节点有一个属性,标记当前表格显示隐藏的状态,这个需要在数据处理时加进去,默认全是false,然后在循环时 table 里做这个判断 v-if="index==0 || item.showdom" 第一位的展示或者showdom为true
  3. 最后就开始在滚动事件里做处理 重新优化下,原来的思路在删除dom时可能会由于数据的问题产生bug,重新整理了思路,展示dom正常还是那个判断条件:scrolltop+屏幕可视高度如果大于元素的offsetTop就把showdom = true。删除dom的思路要变一下,首先是删除可视区域上面dom的情况,scrolltop 如果大于元素的offsetTop+元素的offsetHeight说明当前的dom已经移动到可视区域外了,就可以删除了showdom = false。还有一个就是删除可视区域下面的dom,跟显示的条件相反就行。
  4. 记得要在数据回来后主动执行下对比的方法,以免数据的问题,第一个表格的数据没占满可视区域。
  5. 说下这种解决的优缺点吧,优点毋庸置疑,页面不卡了,缺点也显而易见,就是滚动快了会增加白屏时间,但是利大于弊,所以必须这么做。

七、总结

至此这篇文章就结束了,其中每个细节部分都不简单,都要仔细思考,要不我也不会写出来, 如果还有什么问题评论区留言,我尽量解答。希望看完不要忘记点赞,手敲不易。

八、补充

能不用table布局就不用了吧,可以使用display属性模拟table。测试了下,如果横向数据十多条是没什么问题,但是几十条的话,table会比div卡很多。