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

6,619 阅读5分钟

现象:

我们渲染了9999条数据,由于transfer组件会一次性渲染所有数据,所以一次性渲染这么多,卡个几十秒很正常好吧。

解决transfer大数据量渲染有三种方式:

  • 懒加载
  • 虚拟滚动vue-virtual-scroll-list
  • 滚动式分页

懒加载

了解一下element-ui(后面简称为eui)的无限滚动

这就是为了懒加载存在的,然而我们是没办法在业务代码里面给transfer组件添加无限滚动的,因为transfer组件是封装好的。

所以我们只能将eui的transfer组件拉出来,作为业务的自定义组件。

而将eui的组件拉出来,二次修改后作为自定义组件的方式在这篇文章

当我们按照上面的文章,将transfer组件的代码拉出来,作为业务代码的自定义组件之后,文件结构如下

image.png

transfer-pannel.vue就是穿梭框的左右板子,main.vue是中控系统。

编辑transfer-panel.vue

给data添加count属性,无限滚动用。

data() {
  return {
    checked: [],
    allChecked: false,
    query: '',
    inputHover: false,
    checkChangeByUser: true,
    // 无限滚动用,初始只渲染50条
    count:50
  };
},

添加一个方法

methods: {
  load () {
    // 当用户滚动到列表的底部时,额外渲染多50条
    this.count += 50
},

在template中渲染了穿梭框的所有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>

现在我们改造一下,只渲染0-count条数,并且添加v-infinite-scroll 和 infinite-scroll-distance

<el-checkbox-group
    v-infinite-scroll="load" 
    :infinite-scroll-distance="10"
    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.slice(0,count)">
      <option-content :option="item"></option-content>
    </el-checkbox>
    
</el-checkbox-group>

这样,transfer的懒加载就添加完了,然后我们渲染9999条,只需要当用户滚动到底部时,才会更新count,从而渲染更多的checkbox,效果如下

录制_2022_03_14_19_40_50_239.gif

懒加载确实是弄完了,但是当我们点击全选点击任意一个checkbox、或者点击了移动按钮的话,会卡顿很久。

这是因为transfer组件里的全选方法单选方法的算法复杂度太高了,我已经做优化,并且提了pr,内部有人同意了。

优化过程如下:修改transfer-panel.vue文件

全选是updateAllChecked方法:

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]]);
},

优化完之后,全选操作就不会卡顿了

接下来是单选某个checkbox节点时的逻辑

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

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;
        }
    },
}      

优化完之后,点击某个checkbox和全选按钮的耗时就会减少很多

最后是点击移动按钮,在main.vue

移动逻辑是addToRight方法

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;
      }, []);
    }
  }
},

至此,懒加载整体是弄完了,大数据量的代码优化也弄完了。

最终效果如下:

录制_2022_03_14_19_41_01_51.gif

可能有人会问,这懒加载得首次就放入所有的数据

上面懒加载的实现过程如下:

  1. 将全量的数组A(上万条)传入transfer
  2. transfer内部添加懒加载,第一次渲染数组A的前20条,
  3. 当滚动到底部,触发load时,渲染数组A的潜能40条,以此类推

如果我们遇到这样的场景:需要滚动到底部时,去后端加载下一页的数据怎么办? 实现流程如下:

  1. 父组件P在一开始获取服务器数据,第一页前20条,然后传入给transfer
  2. 当transfer滚动到底部时触发load,然后通过$emit(父子组件通信)的方式去通知父组件去后端获取第二页的数据,然后拼接第一页的数据,然后传入到transfer, 这样就能实现懒加载分页啦~~~
    如果这个实现步骤有不懂的,我帮你远程鸭

虚拟滚动

懒加载的方式的缺点就是,当用户一直往下滚的话,一开始只渲染50条,然后随着用户一直往下滚的话,就会渲染100、150...200...1000,列表是真的会渲染出上千条,最终也会卡顿。

而虚拟滚动就能完美解决这问题,永远只渲染50条。

  1. 首先,要把上面懒加载的算法优化保留。
  2. npm i vue-virtual-scroll-list, 各位可以先了解vue-virtual-scroll-list这个组件的使用方式

要是想要了解虚拟滚动的底层原理,可以看这篇文章

体验地址

源码地址

  1. 创建transfer-checkbox-item.vue组件

image.png

内容如下

<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>
  1. 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>
  1. main.vue中接受一个prop->virtualScroll
props:{
    virtualScroll: {
        type: Boolean,
        default: false
    }
}
  1. 在业务代码中使用这个transfer时,得传入:virtual-scroll:true,代表开启虚拟列表功能
<newTransfer v-model="value" :data="data" :virtual-scroll="true"></newTransfer>

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

总效果如下:

录制_2022_03_14_20_40_44_726.gif

滚动式分页

懒加载和虚拟滚动的方式都得将transfer组件拖出来,作为自定义组件。

而滚动式分页只需要在业务代码里面做修改即可实现.

  1. 只显示100条数据。
  2. 下拉显示下100条数据,上拉显示上100条数据。
  3. 当下拉或者上拉增加渲染数据的同时,把新增数据添加进check数组。

这个方案,我这边是有落地的场景的。

各位看官,点个赞赞吧

如果有哪一步不会的,可以加微信:17688172759,远程帮你