为什么须要虚构列表
日常开发中,常常须要解决一个大数据量的列表,可能是须要展现、勾选等;
如果咱们用 html 原生的标签实现,性能到还好。然而当初大多都是用第三方组件库开发 (例如 element-ui),以便晋升开发效率;
如果咱们同时展现、勾选千条数据的时候,页面将会卡主,甚至崩掉;
虚构列表的计划正是为了解决这类前端大数据量展现、操作卡顿的问题;
虚构列表原理
虚构列表只对局部区域进行渲染,对区域外的 DOM 进行销毁操作。
如果用户高低滚动,那 DOM 元素就会始终创立、销毁(做好笔记,这是考试重点)。
实现多选框组件(万条数据不卡顿)
网上插件很多,不反复造轮子了,本次案例应用的插件 vue-virtual-scroll-list,具体参数配置请查看官网
要实现大数据量的展现和勾选
1. 外围点在 vue 的 mixins 中注册事件 dispatch (这是官网的案例代码);
2. 在组件中应用 $on 订阅虚构滚动插件 data-component 的勾选事件;
3. 在虚构列表的子组件中,在 mounted 钩子扭转勾选状态,因为随着列表滚动,子组件在一直被创立、销毁;’
tips:相干常识,请查看相应链接。本案例未实现默认值、全选等性能,你能够依据理论业务,对组件二次革新。
调用成果及代码
<!--
* @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>
实现穿梭框组件(万条数据不卡顿)
穿梭框组件的外围代码跟上述的多选框组件统一,只不过多了搜寻、勾选去重等性能,整体的性能更为简单;
你能够依据理论业务,增加插槽、批改默认值等;
调用成果及代码
<!--
* @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>
仓库源码
p-virtual-check
p-virtual-transfer