为什么须要虚构列表

日常开发中,常常须要解决一个大数据量的列表,可能是须要展现、勾选等;

如果咱们用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