为什么需要虚拟列表
日常开发中,经常需要处理一个大数据量的列表,可能是需要展示、勾选等;
如果我们用html原生的标签实现,性能到还好。但是现在大多都是用第三方组件库开发(例如element-ui),以便提升开发效率;
如果我们同时展示、勾选千条数据的时候,页面将会卡主,甚至崩掉;
虚拟列表的方案正是为了解决这类前端大数据量展示、操作卡顿的问题;
虚拟列表原理
虚拟列表只对部分区域进行渲染,对区域外的DOM进行销毁操作。
如果用户上下滚动,那DOM元素就会一直创建、销毁(做好笔记,这是考试重点)。
show2.png实现多选框组件(万条数据不卡顿)
网上插件很多,不重复造轮子了,本次案例使用的插件 vue-virtual-scroll-list,详细参数配置请查看官网
要实现大数据量的展示和勾选
1.核心点在vue的 mixins 中注册事件 dispatch (这是官网的案例代码);
2.在组件中使用 $on 订阅虚拟滚动插件 data-component 的勾选事件;
3.在虚拟列表的子组件中,在mounted钩子改变勾选状态,因为随着列表滚动,子组件在不断被创建、销毁;’
tips:相关知识,请查看相应链接。本案例未实现默认值、全选等功能,你可以根据实际业务,对组件二次改造。
调用效果及代码
show2.gif<!--
* @Date: 2022-05-13 13:55:59
* @Author: surewinT 840325271@qq.com
* @LastEditTime: 2022-05-13 22:09:43
* @LastEditors: surewinT 840325271@qq.com
* @Description: 调用页面
-->
<template>
<div class="">
<p-virtual-check :dataSources="dataList" @check-change="checkChange" />
</div>
</template>
<script>
import PVirtualCheck from "@/components/p-virtual-check";
export default {
components: {
"p-virtual-check": PVirtualCheck,
},
props: [],
data() {
return {
dataList: [],
};
},
mounted() {
this.getData();
},
watch: {},
methods: {
// 模拟数据获取
getData() {
let arr = [];
for (let i = 0; i < 10000; i++) {
arr.push({
label: `名称:${i + 1}`,
value: `value${i + 1}`,
});
}
this.dataList = arr;
},
checkChange(result) {
console.log("勾选:", result);
},
},
};
</script>
<style lang="scss" scoped></style>
全局混入mixins(核心代码)
/*
* @Date: 2022-01-07 16:21:56
* @Author: surewinT 840325271@qq.com
* @LastEditTime: 2022-05-13 22:25:00
* @LastEditors: surewinT 840325271@qq.com
* @Description: mixin 混入
*/
import Vue from 'vue'
Vue.mixin({
data() {
return {}
},
methods: {
// 组件事件传递
dispatch(componentName, eventName, ...rest) {
let parent = this.$parent || this.$root
let name = parent.$options.name
while (parent && (!name || name !== componentName)) {
parent = parent.$parent
if (parent) {
name = parent.$options.name
}
}
if (parent) {
parent.$emit.apply(parent, [eventName].concat(rest))
}
}
}
})
组件源码(p-virtual-check)
目录结构
├── p-virtual-check
├── index.vue # 组件入口
├── listItem.vue # 子组件-勾选框
index.vue
<!--
* @Date: 2022-05-13 14:01:24
* @Author: surewinT 840325271@qq.com
* @LastEditTime: 2022-05-13 22:10:20
* @LastEditors: surewinT 840325271@qq.com
* @Description: 虚拟列表-多选框组件
-->
<template>
<div class="">
<virtual-list
class="check-virtual-list"
:keeps="40"
data-key="value"
:data-sources="dataSources"
:data-component="dataComponent"
:extra-props="extraProps"
/>
</div>
</template>
<script>
import VirtualList from "vue-virtual-scroll-list";
import ListItem from "./listItem.vue";
export default {
name: "p-virtual-check",
components: {
"virtual-list": VirtualList,
},
props: {
dataSources: Array,
},
data() {
return {
dataComponent: ListItem,
// 传入组件的额外参数
extraProps: {},
checkMap: {},
};
},
mounted() {
this.setExtraProps();
// 订阅勾选事件
this.$on("virtual-check-change", (key, val) => {
this.checkMap[key] = val;
this.setExtraProps();
this.$emit("check-change", this.checkMap);
});
},
watch: {},
methods: {
setExtraProps() {
this.extraProps["checkMap"] = this.checkMap;
},
},
};
</script>
<style lang="scss" scoped>
.check-virtual-list {
height: 400px;
width: 200px;
overflow: auto;
padding: 0 10px;
}
</style>
listItem.vue
<!--
* @Date: 2022-01-11 16:54:51
* @Author: surewinT 840325271@qq.com
* @LastEditTime: 2022-05-13 22:09:50
* @LastEditors: surewinT 840325271@qq.com
* @Description: 勾选框
-->
<template>
<div class="">
<el-checkbox v-model="checked" @change="change">
{{ source.label }}
</el-checkbox>
</div>
</template>
<script>
export default {
components: {},
props: {
source: {
type: Object,
default() {
return {};
},
},
// 列表的全部勾选情况
checkMap: Object,
},
data() {
return {
checked: false,
};
},
mounted() {
// 回显勾选状态
this.checkChange();
},
watch: {},
methods: {
// 勾选改变
change(val) {
// 通过全局混入函数,调用对应组件的函数
this.dispatch(
"p-virtual-check",
"virtual-check-change",
this.source.value,
val
);
},
// 选择改变
checkChange() {
this.checked = this.checkMap[this.source.value] === true ? true : false;
},
},
};
</script>
<style lang="scss" scoped></style>
实现穿梭框组件(万条数据不卡顿)
穿梭框组件的核心代码跟上述的多选框组件一致,只不过多了搜索、勾选去重等功能,整体的功能更为复杂;
你可以根据实际业务,添加插槽、修改默认值等;
调用效果及代码
show3.gif<!--
* @Date: 2022-05-13 13:55:59
* @Author: surewinT 840325271@qq.com
* @LastEditTime: 2022-05-13 22:10:59
* @LastEditors: surewinT 840325271@qq.com
* @Description: 调用页面
-->
<template>
<div class="">
<p-virtual-transfer
data-value="id"
data-label="name"
:basedata="basedata"
v-model="result"
/>
</div>
</template>
<script>
import PVirtualTransfer from "@/components/p-virtual-transfer";
export default {
components: {
"p-virtual-transfer": PVirtualTransfer,
},
props: [],
data() {
return {
basedata: [],
result: ["value1", "value2", "value4"],
};
},
mounted() {
this.getData();
},
watch: {
result() {
console.log("结果:", this.result);
},
},
methods: {
// 模拟数据获取
getData() {
let arr = [];
for (let i = 0; i < 10000; i++) {
arr.push({
name: `名称:${i + 1}`,
id: `value${i + 1}`,
});
}
this.basedata = arr;
},
},
};
</script>
<style lang="scss" scoped>
</style>
组件源码(p-virtual-transfer)
目录结构
├── p-virtual-transfer
├── index.vue # 组件入口
├── listItem.vue # 子组件-勾选框
├── virtualList.vue # 子组件-列表
index.vue
<!--
* @Date: 2022-01-11 11:02:27
* @Author: surewinT 840325271@qq.com
* @LastEditTime: 2022-05-13 22:10:27
* @LastEditors: surewinT 840325271@qq.com
* @Description: 虚拟滚动-穿梭框组件
-->
<template>
<div class="root">
<!-- 左侧 -->
<virtual-list
:data-value="dataValue"
:data-label="dataLabel"
ref="Middle"
title="标题1"
:basedata="leftData"
@check-change="leftChange"
/>
<!-- 中间按钮 -->
<div class="game-btn">
<el-button
icon="el-icon-arrow-left"
type="primary"
title="移到左边"
@click="move('left')"
></el-button>
<el-button
icon="el-icon-arrow-right"
type="primary"
title="移到右边"
@click="move('right')"
></el-button>
</div>
<!-- 右侧 -->
<virtual-list
ref="Right"
title="标题2"
:data-value="dataValue"
:data-label="dataLabel"
:basedata="rightData"
@check-change="rightChange"
/>
</div>
</template>
<script>
import VirtualList from "./virtualList.vue";
export default {
components: {
"virtual-list": VirtualList,
},
model: {
prop: "value",
event: "input",
},
props: {
value: {
type: Array,
default: () => {
return [];
},
},
basedata: {
type: Array,
default: () => {
return [];
},
},
dataValue: {
type: String,
default: "value",
},
dataLabel: {
type: String,
default: "label",
},
},
data() {
return {
leftData: [],
leftSelect: [],
rightData: [],
rightSelect: [],
};
},
mounted() {
this.initData(this.value);
},
watch: {
basedata() {
this.initData(this.value);
},
},
methods: {
// 初始化数据
initData(value) {
let middleArr = [];
let rightArr = [];
let valueMap = {};
value.forEach((res) => {
valueMap[res] = true;
});
this.basedata.forEach((res) => {
if (valueMap[res[this.dataValue]]) {
rightArr.push(res);
} else {
middleArr.push(res);
}
});
this.leftData = middleArr;
this.rightData = rightArr;
},
// 左侧勾选
leftChange(list) {
this.leftSelect = list;
},
// 右侧勾选
rightChange(list) {
this.rightSelect = list;
},
// 左右移动
async move(type) {
let value = [];
if (type == "right") {
if (this.leftSelect.length == 0) {
this.$message("未勾选任何数据");
return;
}
value = [...this.value, ...this.leftSelect];
}
if (type == "left") {
if (this.rightSelect.length == 0) {
this.$message("未勾选任何数据");
return;
}
value = this.value.filter((res) => {
return !this.rightSelect.includes(res);
});
}
this.initData(value);
this.$emit("input", value);
},
},
};
</script>
<style lang="scss" scoped>
.root {
display: flex;
flex: 1;
width: 910px;
.game-left {
border-right: 1px dashed #ccc;
}
.game-btn {
display: flex;
align-items: center;
margin: 0 10px;
}
}
</style>
listItem.vue
<!--
* @Date: 2022-01-11 16:54:51
* @Author: surewinT 840325271@qq.com
* @LastEditTime: 2022-05-13 21:50:26
* @LastEditors: surewinT 840325271@qq.com
* @Description: 勾选框
-->
<template>
<div class="">
<el-checkbox v-model="checked" @change="change">
{{ source[dataLabel] }}
</el-checkbox>
</div>
</template>
<script>
export default {
components: {},
props: {
source: Object,
dataLabel: String,
dataValue: String,
checkMap: Object,
},
data() {
return {
checked: false,
};
},
mounted() {
// 回显勾选状态
this.checkChange();
},
watch: {},
methods: {
// 勾选改变
change(val) {
this.dispatch(
"p-virtual-transfer",
"virtual-transfer-check-change",
this.source[this.dataValue],
val
);
},
// 选择改变
checkChange() {
this.checked =
this.checkMap[this.source[this.dataValue]] === true ? true : false;
},
},
};
</script>
<style lang="scss" scoped></style>
virtualList.vue
<!--
* @Date: 2022-01-11 11:02:27
* @Author: surewinT 840325271@qq.com
* @LastEditTime: 2022-05-13 21:50:46
* @LastEditors: surewinT 840325271@qq.com
* @Description: 列表
-->
<template>
<div class="check-root">
<!-- 头部 -->
<div class="header">
<el-checkbox v-model="checkAll" @change="changeAll">
{{ title }}
</el-checkbox>
<div class="num">{{ checkNum }}</div>
</div>
<div class="search" v-if="showSearch">
<el-input
size="small"
clearable
placeholder="支持 lable / value 搜索"
v-model="searchName"
@input="inputChange"
>
</el-input>
</div>
<!-- 列表 -->
<div class="main" :style="{ width: width }">
<virtual-list
class="check-virtual-list"
:keeps="50"
:key="virtualKey"
:data-key="dataValue"
:data-sources="dataSources"
:data-component="dataComponent"
:extra-props="extraProps"
/>
</div>
</div>
</template>
<script>
import VirtualList from "vue-virtual-scroll-list";
import List from "./listItem.vue";
export default {
name: "p-virtual-transfer",
components: {
"virtual-list": VirtualList,
},
props: {
basedata: {
type: Array,
default: () => {
return [];
},
},
width: {
type: String,
default: "280px",
},
title: {
type: String,
default: "",
},
showSearch: {
type: Boolean,
default: true,
},
dataValue: {
type: String,
default: "value",
},
dataLabel: {
type: String,
default: "label",
},
},
data() {
return {
// 头部
checkAll: false,
searchName: "",
// 列表
dataSources: [],
dataComponent: List,
extraProps: {
checkMap: {},
dataLabel: this.dataLabel,
dataValue: this.dataValue,
},
virtualKey: 0, // 用于强制更新视图
// 其他
checkMap: {},
allCheckMap: {},
};
},
mounted() {
this.initData();
// 订阅勾选事件
this.$on("virtual-transfer-check-change", (key, val) => {
this.checkMap[key] = val;
this.extraProps["checkMap"] = this.checkMap;
this.handleResult();
});
},
watch: {
basedata() {
this.initData();
},
},
computed: {
checkNum() {
let num = 0;
for (let key in this.checkMap) {
this.checkMap[key] ? num++ : "";
}
return `${num}/${this.basedata.length}`;
},
},
methods: {
// 初始化数据
initData() {
this.$nextTick(() => {
this.checkAll = false;
this.allCheckMap = {};
this.checkMap = {};
this.extraProps["checkMap"] = this.checkMap;
this.basedata.forEach((res) => {
this.allCheckMap[res[this.dataValue]] = true;
});
this.dataSources = this.basedata;
this.virtualKey++;
});
},
// 全选
changeAll(val) {
val
? (this.checkMap = JSON.parse(JSON.stringify(this.allCheckMap)))
: (this.checkMap = {});
this.extraProps["checkMap"] = this.checkMap;
this.virtualKey++;
this.handleResult();
},
// 搜索
inputChange() {
this.$nextTick(() => {
this.checkAll = false;
this.allCheckMap = {};
// 正则匹配
let arr = [];
let searcReg = new RegExp(String(this.searchName));
this.basedata.forEach((res) => {
if (
searcReg.test(String(res[this.dataLabel])) ||
searcReg.test(String(res[this.dataValue]))
) {
this.allCheckMap[res[this.dataValue]] = true;
arr.push(res);
}
});
this.dataSources = arr;
this.virtualKey++;
});
},
// 处理结果
handleResult() {
let arr = [];
for (let key in this.checkMap) {
this.checkMap[key] === true ? arr.push(key) : "";
}
this.$emit("check-change", arr);
},
},
};
</script>
<style lang="scss" scoped>
.check-root {
border: 1px solid #ebeef5;
.header {
padding: 0 20px;
height: 40px;
line-height: 40px;
background-color: #f5f7fa;
display: flex;
justify-content: space-between;
.num {
color: #909399;
}
}
.search {
padding: 5px 20px;
::v-deep .el-input__inner {
border-radius: 10px;
}
}
.main {
background-color: #fff;
padding: 0 10px;
.check-virtual-list {
height: 400px;
overflow: auto;
padding: 0 10px;
}
}
}
</style>
网友评论