2024.02 设计一个有缓存功能的下拉组件

要求:

  • 使用element-ui
  • 实现接口缓存-使用map实现
  • 实现自定义label和值,label可以自己设置或者按照code-name方式展示-自定义展示字段或者根据传入的参数格式化label
  • 支持自己添加下拉项-在查询结果上concat一下自定义的下拉项
  • 支持设置默认值-v-model绑定
  • 支持自定义参数查询
  • 2024年4月7日更新-增加是否缓存参数

思考:

  • 怎么写一个最近最久未使用算法来比较合理的删除已缓存元素,缓存大小需要有个设置,不能无限制增大,影响内存
  • 缓存key
  • 使用v-bind和v-on继承原有组件的属性和事件

实现:

cache.js

class Cache {
  constructor(maxSize) {
    this.maxSize = maxSize
    this.map = new Map()
  }

  set(key, value) {
    const hasKey = this.map.has(key)
    this.map.set(key, value)

    if (!hasKey) {
      this.removeOldestEntry()
    }
  }
  
  get(key) {
    const value = this.map.get(key)
    // get时候重新设置时间
    if(value){
      this.map.set(key, {value:value.value,time:new Date().getTime()})
    }
    return this.map.get(key)
  }

  remove(key) {
    this.map.delete(key)
  }

  removeOldestEntry() {
    if (this.map.size <= this.maxSize) {
      return
    }
    // 查找到需要删除元素得key-暴力删除每次删除第一个
    // const keys = Array.from(this.map.keys())
    // if (keys.length) {
    //   this.remove(keys[0])
    // }
    const keys = Array.from(this.map.keys())
    const values = Array.from(this.map.values()).map(item => item.time)
    //  循环每个key,找到最久未使用的key,返回e
    const index = findMinIndex(values)
    this.remove(keys[index])
  }
}

function findMinIndex(arr) {
  const min = Math.min(...arr)
  for (let i = 0; i < arr.length; i++) {
    if (arr[i] === min) {
      return i
    }
  }
}

export default Cache

测试cache,为什么要写一个异步的方法测试?

因为如果设置为同步的,在代码执行的一瞬间相差还不到一秒,查找不到最久未使用的Key

const cache = new Cache(5)
cache.set('a', { value: 1, time: new Date().getTime() })
cache.set('b', { value: 2, time: new Date().getTime() })
cache.set('c', { value: 3, time: new Date().getTime() })
cache.set('d', { value: 4, time: new Date().getTime() })
cache.set('e', { value: 5, time: new Date().getTime() })

console.log(cache);
cache.get('a') // undefined
cache.get('b') // {value:2,time:162071210000}
cache.get('c') // {value:3,time:162071210000}
cache.get('d') // {value:4,time:162071210000}
cache.get('a') // {value:4,time:162071210000}
setTimeout(() => {
  cache.set('f', { value: 6, time: new Date().getTime() })
}, 3000);
// cache.set('g', { value: 7, time: new Date().getTime() })
console.log(cache);

组件实现

<template>
  <div class="hy-select">
    <z-select
      ref="refInputSelect"
      v-model="inputValue"
      style="width:100%"
      :remote-method="searchRemote"
      v-bind="$attrs"
      v-on="$listeners"
      @change="handleChange"
    >
      <z-option
        v-for="item in options"
        :key="item[fieldType] || item"
        :label="showLabel(item)"
        :value="item[fieldType] || item"
        :disabled="item.disabled"
      >
        <slot :scope="item"></slot>
      </z-option>
    </z-select>
  </div>
</template>

<script>
import { debounce } from 'lodash'
import Cache from '@/utils/cache.js'
import request from '@/utils/request'
import { HostName } from '@/config/env'
// 增加缓存
const cache = new Cache(100)
export default {
  name: 'CommonSelect',
  props: {
    value: {
      type: [String, Number, Array]
    },
    // 接收接口的所有参数值
    param: {
      type: Object,
      default: () => {}
    },
    // 搜索api
    searchApi: {
      type: String,
      required: true
    },
    // 搜索方法
    searchMethod: {
      type: String,
      default: 'post'
    },
    // 搜索关键词key
    searchKey: {
      type: String,
      default: 'name'
    },
    // 自定义项
    addOptions: {
      type: Array,
      default: () => []
    },
    // 默认取值字段-兼容其他下拉
    fieldType: {
      type: String,
      default: 'id'
    },
    // 展示标签连接符
    fieldSplitor: {
      type: String,
      default: '-'
    },
    // 展示标签
    fieldLabels: {
      type: [Array, Function],
      default: () => ['code', 'name']
    },
    // 默认value
    defaultValue: {
      type: [String, Number, Array],
      default: ''
    },
    // 默认options
    defaultOptions: {
      type: Array,
      default: () => []
    },
    // 是否缓存结果
    isCache: {
      type: Boolean,
      default: true
    }
  },
  data() {
    return {
      inputValue: '',
      options: []
    }
  },
  watch: {
    defaultOptions: {
      handler(val) {
        this.options = this.addOptions.concat(val)
      }
    }
  },
  mounted() {
    this.inputValue = this.value

    if (this.defaultValue && this.defaultOptions.length) {
      this.inputValue = this.defaultValue
      this.options = this.addOptions.concat(this.defaultOptions)
    }
    // 不是远程搜索,先查询一次
    if (!this.remote) {
      this.getData()
    }
  },
  methods: {
    showLabel(item) {
      const fields = []
      if (typeof this.fieldLabels === 'function') {
        return this.fieldLabels(item)
      }

      this.fieldLabels.forEach(field => {
        if (item[field]) {
          fields.push(item[field])
        }
      })
      return fields.join(this.fieldSplitor) || item
    },
    searchRemote: debounce(function(keyword) {
      if (!keyword) return
      this.getData(keyword)
    }, 500),
    async getData(query) {
      const searchKey = this.searchKey
      const params = { ...this.param, [searchKey]: query }

      const res = await this.searchRequest(params)
      if (res.status) {
        this.options = this.addOptions.concat(res.result)
      }
    },
    handleChange(val) {
      this.$emit('change', val, this.options)
    },
    // 搜索方法封装
    searchRequest(data) {
      // 默认使用name作为关键词参数,使用api地址+参数作为缓存key
      const cacheKey = `${this.searchApi}-${this.searchMethod}-${JSON.stringify(data)}`
      const cachedValue = cache.get(cacheKey)
      // 缓存结果&有缓存结果
      if (this.isCache) {
        if (cachedValue) {
          return new Promise((resolve, reject) => {
            resolve(cachedValue.value)
          })
        } else {
          return this.sendRequest(data, cacheKey)
        }
      } else {
        // 不缓存结果,实时请求
        return request({
          url: `${HostName}${this.searchApi}`,
          method: this.searchMethod,
          data
        })
      }
    },
    // sendRequest
    sendRequest(data, cacheKey) {
      return request({
        url: `${HostName}${this.searchApi}`,
        method: this.searchMethod,
        data
      }).then(res => {
        // 链式处理请求的结果,将结果缓存,再返回
        cache.set(cacheKey, {value:res,time:new Date().getTime()})
        return new Promise((resolve, reject) => {
          resolve(res)
        })
      })
    }
  }
}
</script>

<style scoped lang="scss"></style>



使用方式:

<common-select
              v-model="formData.provinceCode"
              :filterable="true"
              :multiple="true"
              :remote="false"
              :searchApi="'/api/provinceQuery'"
              style="width: 90%"
              fieldType="code"
              ></common-select>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容