Element-UI的transfer穿梭框组件数据量大解决方案

 一、面临问题

数据量大,渲染慢,搜索、勾选、关闭、右移卡顿

二、解决方案

1. 总体思路

改写 Element-UI 的 transfer 穿梭框组件,形成自己的自定义组件

2. 具体步骤

2.1 复制 Element-UI 的 transfer 穿梭框组件出来

a. 对 node_modules/element-ui 中文件结构的理解

  • 注册进vue中的element-ui组件使用的是打包后的文件夹:lib
  • packages、src、types是源码,仅供开发者查阅。
  • 我们主要需要使用 packages 里的源码

image

b. 复制出 packages 里的 transfer 组件

image

  • transfer-pannel.vue就是穿梭框的左右板子,main.vue是中控系统,index.js 是组件导出

2.2 编辑transfer-panel.vue文件,优化全选和单选

a. 优化全选卡顿问题

  • 原来代码
    updateAllChecked() {
        const checkableDataKeys = this.checkableData.map(item => item[this.keyProp]);
        // 这里是O(n^2)的时间复杂度
        this.allChecked = checkableDataKeys.length > 0 &&
          checkableDataKeys.every(item => this.checked.indexOf(item) > -1);
    },
  • 修改后代码
    updateAllChecked() {
        let checkObj = {};
        this.checked.forEach((item) => {
          checkObj[item] = true;
        });
        this.allChecked =
          this.checkableData.length > 0 &&
          this.checked.length > 0 &&
          this.checkableData.every((item) => checkObj[item[this.keyProp]]);
    },

b. 优化单选卡顿问题

  • 源来代码
    watch: {
        checked(val, oldVal) {
            this.updateAllChecked();
            if (this.checkChangeByUser) {
              // O(n^2)的时间复杂度
              const movedKeys = val.concat(oldVal)
                .filter(v => val.indexOf(v) === -1 || oldVal.indexOf(v) === -1);
              this.$emit('checked-change', val, movedKeys);
            } else {
              this.$emit('checked-change', val);
              this.checkChangeByUser = true;
            }
        },
    }
  • 修改后代码
    watch: {
        checked(val, oldVal) {
            this.updateAllChecked();
            let newObj = {};
            val.every((item)=>{
              newObj[item] = true;
            });
            let oldObj = {};
            oldVal.every((item)=>{
              oldObj[item] = true;
            });
            if (this.checkChangeByUser) {
              // O(n)
              const movedKeys = val.concat(oldVal)
                .filter(v => newObj[v] || oldVal[v]);
              this.$emit('checked-change', val, movedKeys);
            } else {
              this.$emit('checked-change', val);
              this.checkChangeByUser = true;
            }
        },
    }  

2.3 修改main.vue文件,优化移动卡顿

  • 原来代码
    addToRight() {
        let currentValue = this.value.slice();
        const itemsToBeMoved = [];
        const key = this.props.key;
        this.data.forEach(item => {
          const itemKey = item[key];
          // O(n^2)
          if (
            this.leftChecked.indexOf(itemKey) > -1 &&
            this.value.indexOf(itemKey) === -1
          ) {
            itemsToBeMoved.push(itemKey);
          }
        });
        currentValue = this.targetOrder === 'unshift'
          ? itemsToBeMoved.concat(currentValue)
          : currentValue.concat(itemsToBeMoved);
        this.$emit('input', currentValue);
        this.$emit('change', currentValue, 'right', this.leftChecked);
    },
  • 修改后代码
    addToRight() {
        let currentValue = this.value.slice();
        const itemsToBeMoved = [];
        const key = this.props.key;
        let leftCheckedKeyPropsObj = {};
        this.leftChecked.forEach((item) => {
          leftCheckedKeyPropsObj[item] = true;
        });
        let valueKeyPropsObj = {};
        this.value.forEach((item) => {
          valueKeyPropsObj[item] = true;
        });
        this.data.forEach((item) => {
          const itemKey = item[key];
          // O(n)
          if (
            leftCheckedKeyPropsObj[itemKey] &&
            !valueKeyPropsObj[itemKey]) {
            itemsToBeMoved.push(itemKey);
          }
        });
        currentValue = this.targetOrder === 'unshift'
          ? itemsToBeMoved.concat(currentValue)
          : currentValue.concat(itemsToBeMoved);
        this.$emit('input', currentValue);
        this.$emit('change', currentValue, 'right', this.leftChecked);
    },
  • 除此之外,还要优化两个computed
     computed: {
      sourceData() {
        let valueObj = {};
        this.value.forEach((item)=>{
          valueObj[item] = true;
        });
        return this.data.filter(
          (item) => !valueObj[item[this.props.key]]
        );
      },
      targetData() {
        if (this.targetOrder === 'original') {
          let valueObj = {};
          this.value.forEach((item)=>{
            valueObj[item] = true;
          });
          let data = this.data.filter(
            (item) => valueObj[item[this.props.key]]
          );
          return data;
        } else {
          return this.value.reduce((arr, cur) => {
            const val = this.dataObj[cur];
            if (val) {
              arr.push(val);
            }
            return arr;
          }, []);
        }
      }
    },

2.4 使用虚拟滚动解决渲染问题

  •  npm i vue-virtual-scroll-list
  • 创建transfer-checkbox-item.vue组件

image

内容如下:

<template>
  <el-checkbox
    class="el-transfer-panel__item"
    :label="source[keyProp]"
    :disabled="source[disabledProp]">
    <option-content :option="source"></option-content>
</el-checkbox>
</template>

<script>
  import ElCheckbox from 'element-ui/packages/checkbox';
  export default {
    name: 'transfer-checkbox-item',
    props: {
      index: { // index of current item
        type: Number
      },
      source: { // here is: {uid: 'unique_1', text: 'abc'}
        type: Object,
        default() {
          return {};
        }
      },
      keyProp: {
        type: String
      },
      disabledProp: {
        type: String
      }
    },
    components: {
      ElCheckbox,
      OptionContent: {
        props: {
          option: Object
        },
        render(h) {
          const getParent = vm => {
            if (vm.$options.componentName === 'ElTransferPanel') {
              return vm;
            } else if (vm.$parent) {
              return getParent(vm.$parent);
            } else {
              return vm;
            }
          };
          const panel = getParent(this);
          const transfer = panel.$parent || panel;
          return panel.renderContent
            ? panel.renderContent(h, this.option)
            : transfer.$scopedSlots.default
              ? transfer.$scopedSlots.default({ option: this.option })
              : <span>{ this.option[panel.labelProp] || this.option[panel.keyProp] }</span>;
        }
      }
    }
  };
</script>

 

  • transfer-panel.vue做修改 引入两个东西
import Item from './transfer-checkbox-item.vue';
import VirtualList from 'vue-virtual-scroll-list';

// 注册VirtualList
components: {
  'virtual-list': VirtualList
}
  • 初始化定义两个变量
data() {
    return {
        itemComponent: Item,
        virtualListProps: {}
    }
}
  • 定义一个computed->virtualScroll
computed: {
  virtualScroll() {
    return this.$parent.virtualScroll;
  },
}
  • 修改一个computed->keyProp
computed: {
    keyProp() {
        this.virtualListProps.keyProp = this.props.key || 'key';
        return this.props.key || 'key';
    }
}
  • 修改一个computed->disabledProp
computed: {
    disabledProp() {
        this.virtualListProps.disabledProp = this.props.disabled || 'disabled';
        return this.props.disabled || 'disabled';
    }
}
  • 原checkbox集合的渲染方式如下
<el-checkbox-group
    v-model="checked"
    v-show="!hasNoMatch && data.length > 0"
    :class="{ 'is-filterable': filterable }"
    class="el-transfer-panel__list"
      <el-checkbox
        class="el-transfer-panel__item"
        :label="item[keyProp]"
        :disabled="item[disabledProp]"
        :key="item[keyProp]"
        v-for="item in filteredData">
        <option-content :option="item"></option-content>
      </el-checkbox>
</el-checkbox-group>
  • 修改为:
 <el-checkbox-group
    v-model="checked"
    v-show="!hasNoMatch && data.length > 0"
    :class="{ 'is-filterable': filterable }"
    class="el-transfer-panel__list">
    <virtual-list 
      v-if="virtualScroll"
      style="height:100%;overflow-y: auto;"
      :data-key="keyProp"
      :data-sources="filteredData"
      :data-component="itemComponent"
      :extra-props="virtualListProps"
    />
    <template v-else>
      <el-checkbox
        class="el-transfer-panel__item"
        :label="item[keyProp]"
        :disabled="item[disabledProp]"
        :key="item[keyProp]"
        v-for="item in filteredData">
        <option-content :option="item"></option-content>
      </el-checkbox>
    </template>
</el-checkbox-group>
  • main.vue中接受一个prop->virtualScroll
 
props:{
    virtualScroll: {
        type: Boolean,
        default: false
    }
}

 

  • 在业务代码中使用这个transfer时,得传入:virtual-scroll:true,代表开启虚拟列表功能
 
<newTransfer v-model="value" :data="data" :virtual-scroll="true"></newTransfer>

 

至此,应该是没问题的了,渲染十万条都是没问题的。

 
posted @ 2025-09-29 17:06  RHCHIK  阅读(16)  评论(0)    收藏  举报