el-select 性能优化

目录

1.目的

2.原理

3.优点

4.源码

1)安装插件

2) 创建全局组件VirtualListSelect。

a.简单页面也即正常开发的页面中

b.通过表单配置在页面中渲染出来的下拉框


1.目的

为了解决 element-ui 中 el-select 组件在大数据量的情况下出现的性能问题(数据量太大,导致渲染过慢,或造成页面卡顿甚至于卡死) 。

2.原理

模拟虚拟滚动,对 el-select组件结合vue-virtual-scroll-list(vue虚拟列表)插件进行二次封装

3.优点

适用于只查询一次接口,后端一次性把数据返回。 对比其他的优化方案,有以下优点

方案一:后台进行分页;这种。。嗯,那后端人员可能不乐意了,心里想我都把数据返回给你了,加载慢,页面卡顿不应该你前端的问题吗(关我什么事)。ok,为了避免这种情况,我们就不麻烦后端同学在每个下拉框数据返回后进行分页了,那可是个大工程。对比这种方案优点就是

  • 无需后端进行配合,只需要初始化的时候一次性把数据返回给前端即可

方案二:前端懒加载,这种确实可以,我不用此方案就是考虑到数据量过大,那即使页面初始加载的时候较快,但是在页面销毁,简单来说就是切页面的时候,你要销毁所有的dom节点那肯定会出现你的路由已经变化了,但是你的页面还没跳转,造成用户体验不好。对比这种方案我的方案优点

  • 由于不管下拉框如何下拉,至始至终都只渲染自己设置的规定的数量,比如:keeps="20"那这个下拉框始终只会渲染20条下拉数据,vue种dom节点那就渲染20条,对比成千上万,不论是初始化或者销毁速度都会明显的提升,用户体验好。

4.源码

1)安装插件

由于需要使用插件vue-virtual-scroll-list,所以我们先在项目中把插件安装一下;

插件官网地址:vue-virtual-scroll-list - npmvue-virtual-scroll-list - npmvue-virtual-scroll-list - npm

npm install vue-virtual-scroll-list --save

2) 创建全局组件VirtualListSelect。

我使用的环境相对来说比较复杂 ,因为我封装的这个组件主要用在低代码平台中,即下拉框是通过页面配置然后渲染出来的,所以需要接收许多参数,但是在正常开发的页面中不需要接收这么多参数,为了方便读者借鉴,我就两种都记录一下。正常开发的页面已经满足大部分读者的需求。

a.简单页面也即正常开发的页面中

  •  在src/components下创建VirtualListSelect文件夹,文件夹下大概格式如下:

  •  VirtualListSelect.vue文件
<template>
  <div>
    <el-select
      :value="defaultValue"
      popper-class="virtualselect"
      filterable
      :filter-method="filterMethod"
      @visible-change="visibleChange"
      v-bind="$attrs"
      v-on="$listeners"
    >
      <virtual-list
        ref="virtualList"
        class="virtualselect-list"
        :data-key="selectData.value"
        :data-sources="selectArr"
        :data-component="itemComponent"
        :keeps="20"
        :extra-props="{
          label: selectData.label,
          value: selectData.value,
          isRight: selectData.isRight
        }"
      ></virtual-list>
    </el-select>
  </div>
</template>

<script>
import VirtualList from 'vue-virtual-scroll-list';
import itemComponent from './itemComponent';
export default {
  name: 'Select',
  components: {
    'virtual-list': VirtualList
  },
  model: {
    prop: 'defaultValue',
    event: 'change'
  },
  props: {
    selectData: {
      type: Object,
      default() {
        return {};
      }
    }, //父组件传的值
    defaultValue: {
      type: String,
      default: []
    } // 绑定的默认值
  },
  mounted() {
    this.init();
  },
  watch: {
    'selectData.data'() {
      this.init();
    }
  },
  data() {
    return {
      itemComponent: itemComponent,
      selectArr: []
    };
  },
  methods: {
    init() {
      if (!this.defaultValue || this.defaultValue.length === 0) {
        this.selectArr = this.selectData.data;
      } else {
        // 回显问题
        // 由于只渲染20条数据,当默认数据处于20条之外,在回显的时候会显示异常
        // 解决方法:遍历所有数据,将对应回显的那一条数据放在第一条即可
        // 注意:此例子只有单选情况,多选类似,想看实现代码请在低代码情况下会有完善
        this.selectArr = JSON.parse(JSON.stringify(this.selectData.data));
        let obj = {};
        for (let i = 0; i < this.selectArr.length; i++) {
          const element = this.selectArr[i];
          if (
            element[this.selectData.value].toLowerCase() ===
            this.defaultValue.toLowerCase()
          ) {
            obj = element;
            this.selectArr.splice(i, 1);
            break;
          }
        }
        this.selectArr.unshift(obj);
      }
    },
    // 搜索
    filterMethod(query) {
      if (query !== '') {
        this.$refs.virtualList.scrollToIndex(0); //滚动到顶部
        setTimeout(() => {
          this.selectArr = this.selectData.data.filter((item) => {
            return this.selectData.isRight
              ? item[this.selectData.label]
                  .toLowerCase()
                  .indexOf(query.toLowerCase()) > -1 ||
                  item[this.selectData.value]
                    .toLowerCase()
                    .indexOf(query.toLowerCase()) > -1
              : item[this.selectData.label]
                  .toLowerCase()
                  .indexOf(query.toLowerCase()) > -1;
          });
        }, 100);
      } else {
        this.init();
      }
    },
    visibleChange(bool) {
      if (!bool) {
        this.$refs.virtualList.reset();
        this.init();
      }
    }
  }
};
</script>
<style lang="scss">
.virtualselect {
  // 设置最大高度
  &-list {
    max-height: 245px;
    overflow-y: auto;
  }
  .el-scrollbar .el-scrollbar__bar.is-vertical {
    width: 0 !important;
  }
}
</style>

  •  itemComponent.vue 文件

<template>
  <div>
    <el-option
      :key="label + value"
      :label="source[label]"
      :value="source[value]"
    >
      <span>{{ source[label] }}</span>
      <span v-if="isRight" style="float:right;color:#939393">{{
        source[value]
      }}</span>
    </el-option>
  </div>
</template>

<script>
export default {
  name: 'item-component',
  props: {
    // index of current item
    // 每一行的索引
    index: {
      type: Number
    },
    // 每一行的内容
    source: {
      type: Object,
      default() {
        return {};
      }
    },
    // 需要显示的名称
    label: {
      type: String
    },
    // 绑定的值
    value: {
      type: String
    },
    // 右侧是否显示绑定的值
    isRight: {
      type: Boolean,
      default() {
        return false;
      }
    }
  },
  mounted() {}
};
</script>

组件封装完成后,我们最好将其注册成全局组件,以便在系统中使用

import VirtualListSelect from './VirtualListSelect';

Vue.component('virtual-list-select', VirtualListSelect);

  • 下面写一个简单的demo 

demo.vue

<template>
  <div class="cw-select">
    <virtual-list-select
      :selectData="selectData"
      v-model="defaultValue"
      multiple
      placeholder="请选择下拉数据"
      clearable
      @change="selectChange"
    ></virtual-list-select>
  </div>
</template>

<script>
export default {
  name: 'virtual-list-select',
  data() {
    return {
      selectData: {
        data: [], // 下拉框数据
        label: 'name', // 下拉框需要显示的名称
        value: 'code', // 下拉框绑定的值
        isRight: false //右侧是否显示
      },
      defaultValue: [] //下拉框选择的默认值
    };
  },
  mounted() {
    this.selectData.data = [];
    for (let i = 0; i < 10000; i++) {
      this.selectData.data.push({ code: 'Test' + i, name: '测试' + i + '' });
    }
  },
  methods: {
    selectChange(val) {
      console.log('下拉框选择的值', val);
    }
  }
};
</script>

b.通过表单配置(低代码平台)在页面中渲染出来的下拉框

  • VirtualListSelect.vue
<template>
  <div class="virtual-list-select">
    <el-select
      :value="value"
      popper-class="virtualselect"
      :multiple="multiple"
      :collapse-tags="collapseTags"
      :placeholder="placeholder"
      :clearable="clearable"
      :disabled="disabled"
      :filterable="filterable"
      :filter-method="filterMethod"
      :remote="remote"
      :remote-method="remoteMethod"
      @visible-change="visibleChange"
      v-bind="$attrs"
      v-on="$listeners"
    >
      <virtual-list
        ref="virtualList"
        class="virtualselect-list"
        :data-key="selectData.value"
        :data-sources="selectArr"
        :data-component="itemComponent"
        :keeps="20"
        :extra-props="{
          label: selectData.label,
          value: selectData.value,
          isRight: selectData.isRight
        }"
      ></virtual-list>
    </el-select>
  </div>
</template>

<script>
import VirtualList from 'vue-virtual-scroll-list';
import itemComponent from './itemComponent';
export default {
  name: 'virtual-list-select',
  components: {
    'virtual-list': VirtualList
  },
  model: {
    prop: 'value',
    event: 'input'
  },
  props: {
    selectData: {
      type: Object,
      default() {
        return {};
      }
    }, //父组件传的值
    value: {
      type: String,
      default: ''
    }, // 绑定的默认值
    multiple: {
      type: Boolean,
      default: false
    },
    placeholder: {
      type: String,
      default: ''
    },
    filterable: {
      type: Boolean,
      default: true
    },
    remote: {
      type: Boolean,
      default: false
    },
    remoteMethod: {
      type: Function,
      default: () => ''
    },
    clearable: {
      type: Boolean,
      default: false
    },
    collapseTags: {
      type: Boolean,
      default: false
    },
    disabled: {
      type: Boolean,
      default: false
    }
  },
  watch: {
    'selectData.data'() {
      this.init();
    }
  },
  data() {
    return {
      itemComponent: itemComponent,
      selectArr: []
    };
  },
  methods: {
    init() {
      if (!this.value || this.value.length === 0) {
        this.selectArr = this.selectData.data;
      } else {
        /**  回显问题
          由于只渲染20条数据,当默认数据处于20条之外,在回显的时候会显示异常
          解决方法:遍历所有数据,将对应回显的那一条数据放在第一条即可 */
        this.selectArr = JSON.parse(JSON.stringify(this.selectData.data));
        if (!this.multiple) {
          // 1.单选
          let obj = {};
          for (let i = 0; i < this.selectArr.length; i++) {
            const element = this.selectArr[i];
            if (
              element[this.selectData.value]?.toLowerCase() ===
              this.value?.toLowerCase()
            ) {
              obj = element;
              this.selectArr.splice(i, 1);
              break;
            }
          }
          this.selectArr.unshift(obj);
        } else {
          // 2.多选
          const selectedArr = [];
          for (let i = 0; i < this.selectArr.length; i++) {
            const element = this.selectArr[i];
            for (let j = 0; j < this.value.length; j++) {
              const item = this.value[j];
              if (
                element[this.selectData.value]?.toLowerCase() ===
                item?.toLowerCase()
              ) {
                selectedArr.push(element);
                this.selectArr.splice(i, 1);
                break;
              }
            }
          }
          this.selectArr.unshift(...selectedArr);
        }
      }
    },
    // 搜索
    filterMethod(query) {
      if (query !== '' && !this.remote) {
        this.$refs.virtualList.scrollToIndex(0); //滚动到顶部
        setTimeout(() => {
          this.selectArr = this.selectData.data.filter((item) => {
            return this.selectData.isRight
              ? item[this.selectData.label]
                  .toLowerCase()
                  .indexOf(query.toLowerCase()) > -1 ||
                  item[this.selectData.value]
                    .toLowerCase()
                    .indexOf(query.toLowerCase()) > -1
              : item[this.selectData.label]
                  .toLowerCase()
                  .indexOf(query.toLowerCase()) > -1;
          });
        }, 100);
      } else {
        this.init();
      }
    },
    visibleChange(bool) {
      if (!bool) {
        this.$refs.virtualList.reset();
        this.init();
      }
    }
  },
  created() {
    this.init();
  }
};
</script>
<style lang="scss" scoped>
.virtualselect {
  // 设置最大高度
  &-list {
    max-height: 245px;
    overflow-y: auto;
  }
  .el-scrollbar .el-scrollbar__bar.is-vertical {
    width: 0;
  }
}
</style>
  •  Readme.md文件
## VirtualListSelect

### 1. 组件说明
* 本组件是对 el-select组件结合vue-virtual-scroll-list(vue虚拟列表)插件的二次封装。原理:模拟虚拟滚动,目的是为了解决 element-ui 中 el-select 组件在大数据量的情况下出现的性能问题(数据量太大,导致渲染过慢,造成页面卡顿甚至于卡死)。
* 插件地址:https://www.npmjs.com/package/vue-virtual-scroll-list
### 2. 实现原理
* 用vue-virtual-scroll-list这个插件去包裹需要循坏展示的标签。这里就是el-option标签。
* 由于插件的 data-component 属性,需要抽离出el-option标签封装成一个组件
### 3. 属性说明
* data-key=“‘id’” 就是绑定的唯一key值
* data-sources=“selectArr” 下拉框的数组
* data-component=“itemComponent” 就是抽离中的el-option组件
* keeps=“20” 渲染的个数(默认30个)
* extra-props 值为对象,可以传入自定义属性进去
### 4. 方法
* 实现模糊搜索功能,使用el-select自带的filterMethod方法
* visible-change事件实现下拉框出现/隐藏时触发虚拟列表重置和把列表重置成全量数据
### 5. 注意点
1. <virtual-list style="max-height: 245px; overflow-y: auto;"
* 这里的样式一定要设置成最大高度,防止数据量少了时候下拉框显示多余空白地方
* 高度要设置成245px,不然会出现两个滚动条,会发生滚动bug
* 一定要设置y轴超出滚动
* select标签使用popper-class自定义一个类名,解决会出现两个滚动条的问题
    
缺点:
* 如果每个选项的长度差距过大,横向宽度会随着滚动变化,这是因为默认只加载20个选项,el-select又是根据所有的optiion中最长的进行填充,如果加载另外20条数据的长度过长时就会出现这种情况。
* 由于只渲染20条数据,当默认数据处于20条之外,在回显的时候会显示异常,目前的解决方法是:遍历所有数据,将对应回显的那一条数据放在第一条即可。

  •  低代码中render函数

render.js

export default {
  name: 'render',
  props: {
    type: String,
    config: Object
  },
  render: function(h) {
    switch (this.type) {
      case 'select':
        // return selectItem(h, this.config);
        return virtualListSelect(h, this.config);
      default:
        return '';
    }
  }
};

// 虚拟下拉列表
function virtualListSelect(h, config) {
  const {
    model,
    key,
    props,
    listeners,
    options,
    optionConfig,
    placeholder,
    syncConfig,
    goods,
    value,
    multiple
  } = config;
  let opl = 'label';
  let opv = 'value';

  if (optionConfig) {
    if (optionConfig.label) {
      opl = optionConfig.label;
    }
    if (optionConfig.value) {
      opv = optionConfig.value;
    }
  }

  const opts = options;

  const on = listeners || {};
  let pps = props || {};
  if (syncConfig) {
    pps = {
      ...pps,
      ...syncConfig()
    };
  }
  return h('virtual-list-select', {
    props: {
      placeholder: placeholder || '请选择',
      filterable: true,
      ...pps,
      value: value,
      selectData: {
        data: opts, // 下拉框数据
        label: opl || 'text', // 下拉框需要显示的名称
        value: opv, // 下拉框绑定的值
        isRight: false //右侧是否显示
      },
      multiple,
      allowCreate: goods.allowCreate,
      disabled: goods.readable === 1 || goods.pageDisabled,
      'multiple-limit': goods.itemMultipleLimit,
      'collapse-tags': true,
      clearable: goods.clearAble,
      'popper-append-to-body': false
    },
    on: {
      ...on,
      change: (val) => {
        goods.value = val;
        if (model) {
          model[key] = val;
        }
        if (on.change) {
          on.change(val);
        }
      }
    }
  });
}

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值