乐趣区

关于javascript:业务实例vue用虚拟列表解决万条数据勾选卡顿问题

为什么须要虚构列表

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

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

退出移动版