美文网首页react & vue & angularvue
【业务实例】vue用虚拟列表解决万条数据勾选卡顿问题

【业务实例】vue用虚拟列表解决万条数据勾选卡顿问题

作者: surewinT | 来源:发表于2022-05-12 22:52 被阅读0次
    show1.jpg

    为什么需要虚拟列表

    日常开发中,经常需要处理一个大数据量的列表,可能是需要展示、勾选等;

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

    仓库源码

    p-virtual-check
    p-virtual-transfer

    相关文章

      网友评论

        本文标题:【业务实例】vue用虚拟列表解决万条数据勾选卡顿问题

        本文链接:https://www.haomeiwen.com/subject/aqtpurtx.html