为什么须要虚构列表
日常开发中,常常须要解决一个大数据量的列表,可能是须要展现、勾选等;
如果咱们用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