Bootstrap

el-table表格(含输入框、下拉框等)-大量数据时,渲染卡顿问题解决方案

背景

项目中,产品不想要分页查询,想要初始就查询出所有数据再用表格把所有数据渲染出来,并支持编辑。
因为表格中嵌套大量的输入框、下拉框等编辑组件,当返回数据有两千条时,进入页面加载特别慢,而且编辑时卡顿。原因就是页面的输入框、下拉框等组件数量太多,渲染需要花费很长时间,导致特别慢,性能问题严重

解决方案

为解决这个问题,
1)首先表格使用滚动分页加载,这样可以大大减少渲染编辑框的数量,降低渲染时间;当滚动条触底后加载下一页数据进行追加,让用户以为所有数据是已经加载出来的。
2)使用css模拟一个虚拟的输入框,简单使用css样式模拟编辑框,相比加载一个编辑框更简单轻便。
3)当点击单元格时,使用js将虚拟输入框进行隐藏,向单元格中插入一个真实的输入框;当真实输入框失去焦点时,再销毁真实输入框,将虚拟输入框展示出来。
这样以来,大大降低渲染时间,一个页面只会存在一个编辑框,也不会操作卡顿。

实现

下面是表格组件(后台返回全部数据后,将数据以allData传给此组件,默认显示10条,滚动时,当滚动条触底,加载下一页数据,在之前10条数据的后面进行累积,共20条,直至数据全部加载)

<template>
    <!-- 表格--滚动分页加载 -->
    <el-table class="my-table" ref="myTable" :data="data" @cell-click="cellClick" :row-key="rowKey">
        <template v-for="col in columns">
            <el-table-column :key="col.prop" :label="col.label" :formatter="col.formatter">
                <template slot-scope="scope">
                    <!-- 模拟输入框样式 -->
                    <div v-if="col.type=='input'" class="v-input" :class="{
                        'v-required': showError(col, scope.row),
                        'gray-color': !scope.row[col.prop],
                        'v-disabled':col.disabled,
                        'is-error': showError(col, scope.row)
                    }">
                        {{scope.row[col.prop] || (disabledCell(col, scope.row) ? '' : col.placeholder)}}
                    </div>
                    <div v-else-if="col.type=='select'">
                        ......
                    </div>
                    <div v-else class="text"
                        v-html="col.formatter ? col.formatter(scope.row,col) : scope.row[col.prop]">
                    </div>
                </template>
            </el-table-column>
        </template>
    </el-table>
</template>
<script>
    import Vue from 'vue';
    import VInput from './VInput.vue'
    export default {
        name: 'V-Table',
        props: {
            // 表格所有数据
            allData: {
                type: Array,
                default: []
            },
            // 列配置参数
            columns: {
                type: Array,
                default: []
            },
            // 每页加载数量
            pageSize: {
                type: Number,
                default: 10
            },
            rowKey: {
                type: String,
                default: 'id'
            },
        },
        data() {
            return {
                currentPage: 0,// 当前页码
                total: 0,// 表格所有数据的总数
                data: [], // 表格展示数据
            }
        },
        computed: {
            // 是否显示 红框提示
            showError() {
                return (col, row) => {
                    if (col.type == 'input' && col.required) {
                        return !row[col.prop]
                    } else if (col.type == 'select' && col.required) {
                        // ......
                    }
                    return false
                }
            },
        },
        watch: {
            allData: {
                deep: true,
                immediate: true,
                handler(newValue) {
                    const currentPage = this.currentPage || 1
                    const total = currentPage * this.pageSize
                    this.data = newValue.slice(0, total)
                }
            },
            // 强制刷新变量
            reload() {
                this.total = this.allData.length
                this.currentPage = 0
                this.$refs.myTable.bodyWrapper.scrollTop = 0
                this.fetchData()
                this.loop()
            },
        },
        mounted() {
            // 表格添加滚动事件
            this.$refs.myTable.bodyWrapper.addEventListener('scroll', this.handleScroll)
        },
        methods: {
            // 滚动加载下一页,将下一页数据和之前数据进行累加
            handleScroll(event) {
                const { scrollTop, scrollHeight, clientHeight } = event.target
                if (Math.ceil(scrollTop) + clientHeight >= scrollHeight) {
                    // 如果数据已全部加载,则跳出
                    if (this.data.length == this.total) {
                        return
                    }
                    this.fetchData()
                }
            },
            fetchData() {
                this.currentPage += 1
                const start = (this.currentPage - 1) * this.pageSize
                const end = start + this.pageSize
                const newData = this.allData.slice(start, end)
                this.data = this.currentPage == 1 ? newData : this.data.concat(newData)
            },
            // 如果滚动高度小于可视范围高度,则继续加载下一页,直至可视区域填充满
            loop() {
                this.$nextTick(() => {
                    const { scrollHeight, clientHeight } = this.$refs.myTable.bodyWrapper
                    if (scrollHeight && clientHeight && scrollHeight <= clientHeight) {
                        if (this.data.length == this.total) {
                            return
                        }
                        this.fetchData()
                        this.loop()
                    }
                })
            },
            // 鼠标点击单元格
            cellClick(row, column, cell, event) {
                const col = this.columns.find(v => v.prop == column.prop)
                if (!col || col.disabled) {
                    return
                }
                if (col.type == 'input') {
                    this.renderInput(cell, row, col)
                } else if (col.type == 'select') {
                    //  ......
                }
            },
            // 隐藏文本div,使用js向单元格里追加el-input组件
            renderInput(cell, row, col) {
                if (cell.getElementByClassName('el-input')?.length) {
                    return
                }
                cell.getElementByClassName('cell')[0].style.display = 'none'
                const myInput = Vue.extend(VInput)
                const vm = new myInput({
                    propsData: {
                        modelValue: row[col.prop],
                        disabled: col.disabled,
                        required: col.required
                    }
                }).$mount()

                cell.appendChild(vm.$el)
                vm.$on('on-input', (val) => {
                    // 输入事件
                })
                vm.$on('on-blur', (val) => {
                    // 失焦事件:销毁el-input,显示文本div
                    setTimeout(() => {
                        if (cell.getElementByClassName('el-input')?.length) {
                            cell.removeChild(cell.getElementByClassName('el-input')[0])
                        }
                        cell.getElementByClassName('cell')[0].style.display = 'block'
                        vm.$destory()
                    }, 100)
                })
                // 输入框获取焦点
                vm.$refs.myInput.focus()
            }

        }
    }
</script>

代码中的VInput也是个组件,如下:

<template>
    <el-input ref="myInput" v-model="modelValue" @input="inputFn" @blur="blurFn"></el-input>
</template>
<script>
    export default {
        name: 'V-Input',
        props: {
            modelValue: {
                type: String,
                default: ''
            }
        },
        data() {
            return {

            }
        },
        methods: {
            inputFn(val) {
                this.$emit('on-input', val)
            },
            blurFn() {
                this.$emit('on-blur', this.modelValue)
            }
        }
    }
</script>

至此,一个表格组件就完成封装了,使用如下

<template>
    <myTable :allData="tableData" :columns="columns" :reload="reload" :pageSize="10" />
</template>

<script>
    import myTable from './test.vue'
    export default {
        name: '',
        components: { myTable },
        data(){
            return {
                tableData: [], // 接口获取,此处省略
                reload: 0,
                columns: [
                    {
                        prop: 'name',
                        disabled: false,
                        label: '名称',
                        type: 'input',
                        placeholder: '请输入'
                    },
                    {
                        prop: 'age',
                        disabled: false,
                        label: '年龄',
                        type: 'select',
                        placeholder: '请选择'
                    }
                ]
            }
        }

    }
</script>

同理,下拉框渲染和输入框一样的方法,大家可以自己去扩展一下

;