二次封装element-select解决数据过多带来的卡顿以及支持首字母搜索

2,166 阅读1分钟

前言✨

该组件是element的el-select组件的二次封装,解决痛点如下:

  1. 支持模糊搜索和首字母搜索(element自带的只支持文字搜索)
  2. 性能优化,某些场景下options数组可能达到数千条甚至上万条,服务端接口没有做分页的情况下,dom渲染数量过多,导致页面和下拉框极其卡顿,还会导致beforeDestroy周期执行时间过长,离开页面的动作变得迟缓,所以我采用了截取的方法,最多展示maxNum(默认100)条,下拉框内滚动每次加5条,大幅度提升渲染速度。并且执行搜索操作时会对全盘数据搜索而不是仅在前100条内搜索。
  3. 开发中经常遇到下拉框选中后获取选中参数的同级所有参数,而element自带的change事件的value只能获取到双向绑定的value,所以网上一般都是采用循环的方式让选中的value和原数组的value值相等才能获取到,这使得代码量增加切性能变差。而本组件提供了optionClick方法可以直接点击获取选中参数的所有同级参数。
  4. 组件内部增加了v-on="$listeners",包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器,在组件中就可以自己增加el-select的事件了

安装及使用

1.首先安装pinyin-match插件

$ npm install pinyin-match --save

2.封装组件方便后期使用

<template>
  <el-select
    v-bind:value="value"
    v-bind="$attrs"
    v-el-select-loadmore="loadMore(maxNum)"
    v-on="$listeners"
    :placeholder="$attrs.placeholder"
    :filter-method="handleFilter"
    @visible-change="visibleChange"
    @focus="clearSelect('focus')"
    @clear="clearSelect('clear')"
    filterable
    clearable
    size="small"
  >
    <el-option
      v-for="(item, index) in optionsList.slice(firstNum, maxNum)"
      :key="item[props.value] + index"
      v-bind="$attrs"
      :label="item[props.label]"
      :value="item[props.value]"
      @click.native="optionClick(item)"
    >
    </el-option>
  </el-select>
</template>

<script>
import PinyinMatch from "pinyin-match";
import { throttle } from "@/utils/tools"; // 封装的函数节流
export default {
  name: "SearchSelect",
  model: {
    prop: "value",
    event: "input",
  },
  props: {
    // 需要绑定的值 等于 v-model
    value: {
      type: [String, Number, Array, Object],
      default: "",
    },
    // 需要循环的数组 必传
    options: {
      type: Array,
      default() {
        return [];
      },
      required: true,
    },
    // el-option参数 必传
    props: {
      type: Object,
      default() {
        return {
          value: "value",
          label: "label",
        };
      },
      required: true,
    },
  },
  data() {
    return {
      optionsList: [],
      copyOptionsList: [],
      firstNum: 0,
      maxNum: 100,
    };
  },
  directives: {
    "el-select-loadmore": (el, binding, vnode) => {
      const DROPDOWN_DOM = el.querySelector(
        ".el-select-dropdown .el-select-dropdown__wrap"
      );
      if (DROPDOWN_DOM) {
        DROPDOWN_DOM.addEventListener(
          "scroll",
          throttle(function () {
            // this.scrollTop - 1 是为了兼容部分浏览器
            const condition =
              this.scrollHeight - this.scrollTop - 1 <= this.clientHeight;
            if (condition) {
              binding.value();
            }
          }),
          200
        );
      }
    },
  },
  watch: {
    // 监听赋值并copy一份
    options: {
      handler(val) {
        this.optionsList = val;
        this.copyOptionsList = JSON.parse(JSON.stringify(val));
        this.showValueMethod();
      },
      deep: true,
    },
    value: {
      handler(val) {
        this.showValueMethod();
      },
    },
  },
  created() {
    this.optionsList = this.options;
    this.copyOptionsList = JSON.parse(JSON.stringify(this.options));
    this.showValueMethod();
  },

  methods: {
    /**
     * @Description: 下拉框支持模糊搜索
     * @Author: JayShen
     * @param {*} val
     */
    handleFilter(val) {
      try {
        if (val) {
          this.firstNum = 0;
          this.maxNum = 100;
          this.optionsList = this.copyOptionsList;
          this.optionsList = this.optionsList.filter((item) =>
              PinyinMatch.match(item[this.props.label], val)
            );
          } else {
              this.optionsList = this.copyOptionsList;
          }
      } catch (error) {
        console.error("模糊音下拉框:", error);
      }
    },
    
    /**
     * @Description: clear、focus事件还原数组
     * @Author: JayShen
     * @param {*}
     */
    clearSelect(type) {
      if (type === "clear") {
        this.firstNum = 0;
        this.maxNum = 100;
      }
      this.optionsList = this.copyOptionsList;
    },
    
    /**
     * @Description: 滚动增加最大条数
     * @Author: JayShen
     * @param {*} n
     */
    loadMore(n) {
      return () => (this.maxNum += 5);
    },
    
    /**
     * @Description: 触发下拉框展示
     * @Author: JayShen
     * @param {*} flag
     */
    visibleChange(flag) {
      if (flag) {
        this.handleFilter();
      }
    },
    
    /**
     * @Description: 解决数据回显问题
     * @Author: JayShen
     * @param {*}
     */
    showValueMethod() {
      if (
        this.value &&
        this.optionsList &&
        this.optionsList.length > this.maxNum
      ) {
        for (let i = 0; i < this.optionsList.length; i++) {
          if (this.optionsList[i][this.props.value] === this.value) {
            if (i > this.maxNum) {
              // 如果value位于数组后面部分,就截取前面的值,增加体验感
              if (this.optionsList.length < i + this.maxNum) {
                this.firstNum = i - this.maxNum;
              } else {
                this.firstNum = i - 5;
              }
              this.maxNum = i + this.maxNum;
            }
            break;
          }
        }
      }
    },
    
    /**
     * @Description: option点击事件
     * @Author: JayShen
     * @param {*} item 当前选中的参数
     */
    optionClick(item) {
      this.$emit("optionClick", item);
    },
  },
};
</script>

3.在页面中使用

<SearchSelect
v-model="id"
:options="list"
:props="{
   label: 'name',
   value: 'id'
}"
@optionClick="optionClick"
placeholder="placeholder"
/>

4.函数节流

export const throttle = (fn, t = 200) => {
  let last
  let timer
  return function () {
    const args = arguments
    const now = +new Date()
    if (last && now - last < t) {
      clearTimeout(timer)
      timer = setTimeout(() => {
        last = now
        fn.apply(this, args)
      }, t)
    } else {
      last = now
      fn.apply(this, args)
    }
  }