el-select 加载一万条记录卡顿、搜索查询回显为value值,怎么办?

295 阅读2分钟

问题开始

本以为超级简单,就把之前几个小页面的功能整合到一个页面,多简单啊,没啥问题!!!

好的,整个一个页面不用一天就搞定了,优化优化样式啥的,完美!

重新测试下之前的接口,发现页面非常卡,震惊几十秒!

轻映录屏 2023-06-29 11-21-30.gif

开始排查

首先查看浏览器加载接口的时间,发现接口请求就十几秒,如此离谱?好的 用postman测试下接口,确定了,没错了后台接口加载慢;

既然初始加载慢,那就loading等等看,但是为啥数据都加上上来了,操作如此卡顿!!!也没干啥啊,就仅仅把几个页面弄个选项卡切换下,点个选择框都点不开呢,得好几秒之后才行,通过performance查看js处理很快,反而是渲染耗时长,一顿操作调试之后发现有一个下拉框一下渲染了一万多条记录,难怪卡死......

看了并测试了vue-virtual-scroller-list等库的功能,感觉都不太适用,想简单快速是实现下目前需要的功能,因为数据要和el-select分组一样的功能,滚动的高度还得适应原本的弹窗高度

简单实现

子组件:

html:

<div ref="scroller"
     class="recycle-scroller"
     :style="{'minHeight': `${viewHeight - preHeight}px`}"
     @scroll="onScroll">
    <div class="recycle-scroller-content"
         :style="{'minHeight': `${totalHeight}px`}">
        <div class="recycle-scroller-wrapper"
             :style="{ transform: `translateY(${translate}px)` }">
            <div class="recycle-scroller-item"
                 :style="{'height': `${itemHeight}px`}"
                 v-for="(item, index) in showList" :key="item[itemKey] || index">
                <slot :item="item"/>
            </div>
        </div>
    </div>
</div>

css:

.recycle-scroller{
    position: relative;
    overflow: auto;
}

.recycle-scroller-content, .recycle-scroller-wrapper{
    position: absolute;
    top: 0;
    width: 100%;
}

js:

Vue.component('recycle-scroller', {
    template: html,
    props: {
        // 是否显示虚拟列表
        flag: {
            type: Boolean,
            default: false
        },
        // 父组件传来的需要渲染的数据
        listData: { 
            type: Array,
            default: () => {
                return []
            },
            require: true
        },
         // 数组的唯一标识字段,提升性能
        itemKey: {
            type: String,
            default: 'id',
            require: true
        },
        // 每条数据的高度
        itemHeight: { 
            type: Number,
            default: 40
        },
        // 可视区域高度
        viewHeight: { 
            type: Number,
            default: 600
        },
        // 可视区域减掉的默认一块的高度
        preHeight: { 
            type: Number,
            default: 0
        }
    },
    data() {
        return {
            showList: [], // 当前渲染的数据
            translate: 0
        }
    },
    watch: {
        flag(newVal) {
            if(newVal) {
                this.onScroll()
            }
        },
        listData(newVal) {
            this.onScroll();
        }
    },
    computed: {
        totalHeight () {
            return this.itemHeight * this.listData.length
        }
    },
    methods: {
        // 滚动 当前显示的数组数据
        onScroll () {
            let me = this
            let scrollTop = me.$refs.recycle.scrollTop
            let viewNum = Math.ceil(me.viewHeight / me.itemHeight)
            // 可视区域 显示第一个条数据序号
            let firstIdx = Math.floor(scrollTop / me.itemHeight)
            // 利用VUE中Diff算法的复用机制,我们可以截取可视区域的上一屏第一条至下一屏最后一条
            let start = firstIdx - viewNum > 0 ? firstIdx - viewNum : 0
            let len = me.listData.length
            let end = firstIdx + (2 * viewNum) < len ? firstIdx + (2 * viewNum) : len
            me.showList = me.listData.slice(start, end)
            me.translate = start * me.itemHeight
        }
    }
});

父组件:

<el-form id="msgForm" ref="msgForm" :model="msgData" label-width="130px">
    <el-form-item label="接收人" prop="roleGroup" :rules="[{ required: true, trigger: 'blur', message: '接收人不能为空' }]">
        <el-select ref="roleSelect" v-model="msgData.roleGroup"
           placeholder="查找用户、角色、组织架构"
           multiple
           filterable
           style="width: 100%"
           :size="size"
           :filter-method="filterMethod"
           @change="roleGroupChange"
           @visible-change="handleVisibleChange">
            <template v-if="virtualData && virtualData.length > 0">
                <recycle-scroller
                    ref="recycle"
                    :flag="flag"
                    :list="virtualData"
                    itemKey="uid"
                    :itemHeight = "30"
                    :viewHeight = "300">
                    <template slot-scope="props">
                        <!-- 分组的组名 使用el-option-group添加判断会出现显示为空白的问题 -->
                        <ul v-if="props.item.level == 0"
                            class="el-select-group__wrap">
                            <li class="el-select-group__title">
                                {{props.item.label}}
                            </li>
                        </ul>
                        <el-option v-else
                               :label="props.item.label"
                               :value="props.item.value"
                               value-key>
                           {{props.item.label}}
                        </el-option>
                    </template>
                </recycle-scroller>
            </template>
        </el-select>
    </el-form-item>
</el-form>
<script>
export default {
    data() {
        return {
            size: 'small',
            msgData: {
                roleGroup: []
            },
            originFlattenData: [],
            // 虚拟列表中渲染的数据
            virtualData: [], 
            flag: false
        }
    },
    methods: {
        // 自定义搜索
        filterMethod: function(query) {
            let me = this;
            if(!!query) {
                let filterData = me.filterTreeData(me.roleGroups,'label',query);
                me.virtualData = me.flatTree(filterData);
                me.$forceUpdate();
            }
        },
        // 接收人 选项列表关闭的时候设置初始所有数据
        handleVisibleChange(visible) {
            if (visible) {
                me.flag = true;
            } else {
                me.flag = false;
                me.virtualData = me.originFlattenData;
            }
            return visible
        },

        roleGroupChange(val) {
            this.$refs.msgForm.validateField('roleGroup');

            let arr = me.originFlattenData.filter(el => {
                return me.msgData.roleGroup.includes(el.value)
            });
            
            arr.map(item => {
                // 搜索查询回显为value值怎么办?看了el-select源码后发现渲染的显示的都是option中的对象 然后有一个cachedOptions,查询选中后的项添加到cachedOptions对象中,回显的时候就能读的这个对象的值了
                me.$refs.roleSelect.cachedOptions.push({
                    currentLabel: item.label,
                    currentValue: item.value,
                    label: item.label,
                    value: item.value
                })
            });
        },
        /**
         * 扁平化树形数组
         * idx 唯一值
         * level 层级
         * */
        flatTree(data,idx=0, level=0) {
            let me = this;
            let result = [];
            let temp = JSON.parse(JSON.stringify(data));
            temp.forEach((item,index) => {
                result.push({
                    ...item,
                    level,
                    uid: '' + idx + index
                });
                if(item.options && item.options.length > 0) {
                    result = result.concat(me.flatTree(item.options,'' + idx + index, level + 1))
                }
            });
            return result;
        },

        /**
         * 根据指定字段过滤树形数组数据
         * field 指定字段
         * value 条件过滤值
         * */
        filterTreeData(data, field, value) {
            let me = this;
            let tree = JSON.parse(JSON.stringify(data));
            return tree.filter(node => {
                // 根据指定字段进行条件判断
                if (node[field].includes(value)) {
                    return true;
                }
                if (node.options) {
                    // 递归处理子节点
                    node.options = me.filterTreeData(node.options, field, value);
                    return (node.options.length > 0); // 返回是否保留该节点
                }
                return false;
            });
        }
        
    }
}
</script>

优化后:

轻映录屏 2023-06-29 11-24-06.gif 后期还需要封装优化下,现在先简单试下