赞
踩
目录
为了解决 element-ui 中 el-select 组件在大数据量的情况下出现的性能问题(数据量太大,导致渲染过慢,或造成页面卡顿甚至于卡死) 。
模拟虚拟滚动,对 el-select组件结合vue-virtual-scroll-list(vue虚拟列表)插件进行二次封装
适用于只查询一次接口,后端一次性把数据返回。 对比其他的优化方案,有以下优点
方案一:后台进行分页;这种。。嗯,那后端人员可能不乐意了,心里想我都把数据返回给你了,加载慢,页面卡顿不应该你前端的问题吗(关我什么事)。ok,为了避免这种情况,我们就不麻烦后端同学在每个下拉框数据返回后进行分页了,那可是个大工程。对比这种方案优点就是
方案二:前端懒加载,这种确实可以,我不用此方案就是考虑到数据量过大,那即使页面初始加载的时候较快,但是在页面销毁,简单来说就是切页面的时候,你要销毁所有的dom节点那肯定会出现你的路由已经变化了,但是你的页面还没跳转,造成用户体验不好。对比这种方案我的方案优点
由于需要使用插件vue-virtual-scroll-list,所以我们先在项目中把插件安装一下;
插件官网地址:vue-virtual-scroll-list - npmvue-virtual-scroll-list - npmvue-virtual-scroll-list - npm
npm install vue-virtual-scroll-list --save
我使用的环境相对来说比较复杂 ,因为我封装的这个组件主要用在低代码平台中,即下拉框是通过页面配置然后渲染出来的,所以需要接收许多参数,但是在正常开发的页面中不需要接收这么多参数,为了方便读者借鉴,我就两种都记录一下。正常开发的页面已经满足大部分读者的需求。
- <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.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>
- <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>
- ## 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.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);
- }
- }
- }
- });
- }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。