现象:
我们渲染了9999条数据,由于transfer组件会一次性渲染所有数据,所以一次性渲染这么多,卡个几十秒很正常好吧。
解决transfer大数据量渲染有三种方式:
- 懒加载
- 虚拟滚动vue-virtual-scroll-list
- 滚动式分页
懒加载
了解一下element-ui(后面简称为eui)的无限滚动
这就是为了懒加载存在的,然而我们是没办法在业务代码里面给transfer组件添加无限滚动的,因为transfer组件是封装好的。
所以我们只能将eui的transfer组件拉出来,作为业务的自定义组件。
而将eui的组件拉出来,二次修改后作为自定义组件的方式在这篇文章
当我们按照上面的文章,将transfer组件的代码拉出来,作为业务代码的自定义组件之后,文件结构如下
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
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>
复制代码
现在我们改造一下,只渲染0-count条数
<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>
复制代码
这样,transfer的懒加载就添加完了,然后我们渲染9999条,只需要当用户滚动到底部时,才会更新count,从而渲染更多的checkbox,效果如下
懒加载确实是弄完了,但是当我们点击全选
、点击任意一个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 = this.dataForShow.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;
}, []);
}
}
},
复制代码
至此,懒加载整体是弄完了,大数据量的代码优化也弄完了。
最终效果如下:
虚拟滚动
懒加载的方式的缺点就是,当用户一直往下滚的话,一开始只渲染50条,然后随着用户一直往下滚的话,就会渲染100、150…200…1000,列表是真的会渲染出上千条,最终也会卡顿。
而虚拟滚动就能完美解决这问题,永远只渲染50条。
- 首先,要把上面懒加载的算法优化保留。
- npm i vue-virtual-scroll-list,
各位可以先了解vue-virtual-scroll-list这个组件的使用方式
要是想要了解虚拟滚动的底层原理,可以看这篇文章
- 创建transfer-checkbox-item.vue组件
内容如下
<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组件拖出来,作为自定义组件。
而滚动式分页只需要在业务代码里面做修改即可实现.
- 只显示100条数据。
- 下拉显示下100条数据,上拉显示上100条数据。
- 当下拉或者上拉增加渲染数据的同时,把新增数据添加进check数组。