el-transfer 数据过多导致页面卡顿优化

731 阅读2分钟

场景描述

在使用 el-transfer 组件时,当左侧列表数据过多,例如下面这个场景需要加载 8000 多条数据时,页面滚动会卡,复选框点击后延迟几秒才能选中...这样体验很差。

优化方案

我想到的预选方案有两种:

  1. 虚拟列表

  2. 滚动加载(原理是模拟分页,当滚动到底部时,加载下一页)

现在分析一下以上这两种方案的优缺点。

虚拟列表

优点:

  1. 无论多少数据页面滚动无压力
  2. 滚动条的长度能代表数据量的多少(滚动条越短,数据量越多)。

缺点:

  1. 需要改造el-transfer组件源码,增加代码量
  2. 改造逻辑复杂
  3. 已选的条目有可能不在dom中,上下滚动过程要做回显处理
  4. 改变搜索条件时,需要回显已选条目

滚动加载

优点:

  1. 不用改造el-transfer组件源码,代码量少
  2. 逻辑简单明了
  3. 已选条目在dom中,上下滚动过程不需要做回显处理

缺点:

  1. 滚动条长度不能代表数据量,随着向下滚动,滚动条逐渐变短
  2. 当一直向下滚动时,列表加载过多的条目,页面一样会卡(但是很少有人向下滚动上千条吧..)

综上分析,我更倾向滚动加载。以下是滚动加载的实现方案,而且是前端分页,所有的数据已经一次性查询回来

代码实现

<template>
  <div id="transfer-form">
    <el-transfer
      ref="myTransfer"
      :titles="['全选', '全选']"
      :props="{
        key: 'key',
        label: 'name'
      }"
      filterable
      :filter-method="filterMethod"
      filter-placeholder="请输入需要检索的表名"
      v-model="selectedData"
      :data="tableList">
    </el-transfer>
  </div>
</template>

<script>
import { debounce } from 'lodash'
export default {
  name: 'BatchMapping',
  data () {
    return {
      tableList: [],
      
      scrollList: [],
      scrollFilterList: [],
      scrollPageSize: 20,
      scrollPageNum: 0,
      scrollQuery: ''
    }
  },
  mounted () {
    const me = this
    // 自定义id方便查找dom
    const transferForm = document.getElementById('transfer-form')
    // 找到滚动容器dom
    const transferPanelList = transferForm.querySelector('.el-transfer-panel__list')
    // 添加scroll事件
    transferPanelList && transferPanelList.addEventListener('scroll', function () {
      // 滚动到底部
      if (transferPanelList.scrollTop + transferPanelList.clientHeight >= transferPanelList.scrollHeight) {
        me.scrollLoadList()
      }
    })
  },
  methods: {
    // 筛选方法永远返回true
    filterMethod (query, item) {
      return true
    },

    effectModel () {
      // 从接口获取列表数据
      _req.getDataSourceTableList(params).then(res => {
        if (res.success) {
          this.initScrollLoad(list)
        }
      })
      }
    },

    // 初始化滚动加载
    initScrollLoad (list) {
      // 如果有取消监听函数,要执行。防止重复监听
      this.scrollQueryWatch && this.scrollQueryWatch()
      const me = this
      // 保存所有的数据到本地
      this.scrollList = Object.freeze(list)
      this.scrollQuery = ''
      // 清空el-transfer组件中leftPanel子组件中的query属性
      this.$refs.myTransfer.$refs.leftPanel.query = ''
      // 监听el-transfer组件中leftPanel子组件中的query属性
      // 保存取消监听函数
      this.scrollQueryWatch = this.$refs.myTransfer.$refs.leftPanel.$watch('query', function (val) {
        // 保存query
        me.scrollQuery = val
        me.scrollDebounceQuery()
      })
      this.scrollFilterList = this.scrollList
      this.scrollPageNum = 1
      this.tableList = []
      this.scrollLoadList()
    },

    // 滚动加载
    scrollLoadList () {
      const startIndex = this.scrollPageSize * (this.scrollPageNum - 1) // 计算开始位置
      const endIndex = startIndex + this.scrollPageSize                 // 计算结束位置
      // 如果开始位置已经超过总数据量 直接返回
      if (startIndex >= this.scrollFilterList.length) return
      const subList = this.scrollFilterList.slice(startIndex, endIndex)
      this.tableList.push(...subList)
      this.scrollPageNum += 1
    },

    // 防抖查询
    scrollDebounceQuery: debounce(function () {
      this.scrollPageNum = 1
      // 默认把选中的数据放入tableList
      this.tableList = this.scrollList.filter(item => this.selectedData.includes(item.key))
      // 过滤出符合条件的数据,注意这里要排除已选中的
      this.scrollFilterList = this.scrollList.filter(item => {
        return (item.name.toLowerCase().indexOf(this.scrollQuery.toLowerCase()) !== -1) && !this.selectedData.includes(item.key)
      })
      this.scrollLoadList()
    }, 1000),
  },
}
</script>

提到 mixins

<template>
  <el-form-item label="提取模型" prop="tableList" class="transfer-from-item" id="transfer-form">
    <el-transfer
      ref="myTransfer"
      :titles="['全选', '全选']"
      :props="{
        key: 'key',
        label: 'name'
      }"
      filterable
      :filter-method="scrollTransferFilterMethod"
      filter-placeholder="请输入需要检索的表名"
      v-model="selectedData"
      :data="form.tableList">
    </el-transfer>
  </el-form-item>
</template>

<script>
import transferScrollMixin from '@/utils/transferScrollMixin'
export default {
  name: 'BatchMapping',
  data () {
    return {
      tableList: []
    }
  },
  methods: {
    effectModel () {
      // 从接口获取列表数据
      _req.getDataSourceTableList(params).then(res => {
        if (res.success) {
          this.initScrollLoad(list)
        }
      })
      }
    }
}
</script>

transferScrollMixin.js

import { debounce } from 'lodash'

export default {
  data () {
    return {
      selectedData: [], // 选中行

      scrollList: [],
      scrollFilterList: [],
      scrollPageSize: 20,
      scrollPageNum: 1,
      scrollQuery: '',
      scrollQueryWatch: null
    }
  },
  mounted () {
    const me = this
    /* el-transfer 的包裹元素,需要自定义 */
    const transferContainer = document.getElementById('transfer-form')
    const transferPanelList = transferContainer.querySelector('.el-transfer-panel__list')
    transferPanelList && transferPanelList.addEventListener('scroll', function () {
      if (transferPanelList.scrollTop + transferPanelList.clientHeight >= transferPanelList.scrollHeight) {
        me.scrollLoadList()
      }
    })
  },
  methods: {
    getTransferVm () {
      /* 自定义 el-transfer 的 ref 值 */
      return this.$refs.myTransfer
    },

    setTransferData (list = [], isPush) {
      if (isPush) {
        this.tableList.push(...list) // 自定义 el-transfer data 属性绑定的属性
      } else {
        this.tableList = list // 自定义 el-transfer data 属性绑定的属性
      }
    },

    scrollTransferFilterMethod (query, item) {
      return true
    },

    initScrollLoad (list) {
      this.scrollQueryWatch && this.scrollQueryWatch()
      const me = this
      this.scrollList = Object.freeze(list)
      this.scrollQuery = ''
      const transferVm = this.getTransferVm()
      transferVm.$refs.leftPanel.query = ''
      // 监听左侧query变化
      this.scrollQueryWatch = transferVm.$refs.leftPanel.$watch('query', function (val) {
        me.scrollQuery = val
        me.scrollDebounceQuery()
      })
      this.scrollFilterList = this.scrollList
      this.scrollPageNum = 1
      this.setTransferData()
      this.scrollLoadList()
    },

    scrollLoadList () {
      const startIndex = this.scrollPageSize * (this.scrollPageNum - 1)
      const endIndex = startIndex + this.scrollPageSize
      if (startIndex >= this.scrollFilterList.length) return
      const subList = this.scrollFilterList.slice(startIndex, endIndex)
      this.setTransferData(subList, true)
      this.scrollPageNum += 1
    },

    scrollDebounceQuery: debounce(function () {
      this.scrollPageNum = 1
      this.setTransferData(this.scrollList.filter(item => this.selectedData.includes(item.key)))
      this.scrollFilterList = this.scrollList.filter(item => {
        return (item.name.toLowerCase().indexOf(this.scrollQuery.toLowerCase()) !== -1) && !this.selectedData.includes(item.key)
      })
      this.scrollLoadList()
    }, 1000)
  }
}