Vue原生下拉组件封装+虚拟滚动功能

介绍

快放假了没事写个demo,仅供参考,里面性能方面还有待优化。大致就是这样的功能和思路。
这里用的是jsx写的一个简单下拉框组件,有需要可以直接拷到项目去。主要处理上万条数据加载的下拉框浏览器卡死的问题。

思路

这里直接上代码吧。主要还是在页面正常引入。

第一步:

<template>
  <div id="app">
    <span @click="add"> 下拉框的值是这个:{{value}}</span>
    <br /> <br />
    <my-select
      v-model="value"
      :options='options'
      :filterProps='filterProps'
      :isVdom='isVdom'
    />

  </div>
</template>

<script>
import MySelect from '@/components/MySelect/select'


export default {
  name: 'App',
  data () {
    return {
      value:'',
      isVdom:false, // 是否开启虚拟滚动下拉
      filterProps:{
        key:'text',
        value:'value'
      },
      options: [
      ]
    }
  },
  components: {
    MySelect
  },
  created() {
       // 模拟数据生成
        let p = new Promise((resolve, reject) => {
          let t = 100 // 需要加载的海量数据 建议开启虚拟滚动后写入上万数据,不开启会卡顿
          let data = []
          for (let i = 0; i < t; i++) {
            data.push({
              text:`我是生成的${i + 1}条数据`,
              value:i+1
            })
          }
          resolve(data)
        })
        p.then((res) => {
          this.options=res
          console.log(this.options)
        
        })
  },
  methods: {
    add(){
      this.value++
    }
  },
}
</script>


这是使用页面的地方,主要就三个属性功能加一个v-model双向绑定值,下面的点击事件可以测试双向绑定的问题。三个属性暂时就是

isVdom 是否开启虚拟滚动列表
filterProps 过滤参数 因为后端返回数据参数可能于组件内部不一致 所以需要内部过滤
options 就是我们传入的数组数据源了。

第二步:

<script>
import scroll from './scroll.js';
import refs from './index.js';

//大数据虚拟加载下拉框
export default {
  model: {
    prop: 'value',
    event: 'change'
  },
  props: {
    // 参数过滤
    filterProps: {
      type: Object,
      default: function() {
        return {
          key:'key',
          value:'value'
        };
      }
    },
    // 下拉框数组
    options: {
      type: Array,
      default: function() {
        return [];
      }
    },
    // 是否启用虚拟滚动
    isVdom:{
       type: Boolean,
        default: function() {
        return false
      }
    },
    value:{
        default: null
    }
  },

  data () {
    return {
      KeyValue:{
        key:'',
        value:''
      },//下拉框显示的内容
      expand:true, //箭头
      isTocuh:false, // 是否点击
      vscroll:null //抽象虚拟滚动对象
    }
  },
  watch:{
      value(newValue){
        // 如果此时双向绑定的值有默认值,需要回显下拉框内容
        if(newValue){
           this.KeyValue.key = refs.filter(newValue)
        }
      },
     'KeyValue.value':{
      handler (newValue, oldValue) {
        this.$emit('change', newValue)
      },
      deep: true, // 对象内部的属性监听,也叫深度监听
      immediate: true // 立即监听
    }
  },
  computed: {
    // 数据更新
        dataList() {
            return refs.init(this.options,this.filterProps);
        }
    },

  // 同步实例状态
  created () {
  },
  mounted() {
     console.log( this.options,' this.options')
  },
  destroy () {

  },
  methods:{
    // 点击小箭头 开始加载数据
    handleClickExpand (){
      this.expand = !this.expand;
     //启用虚拟滚动
        if(this.isVdom){
          if(this.expand){
            // 每次点击需要重新销毁生成新的抽象class
            this.vscroll.destroy(document.getElementById('container'))
          }else{
            this.vscroll=new scroll.Refaa(
                {
                  el: document.getElementById('container'),
                  liHeight: 40, //每一行行数据高度,
                  data: this.dataList,
                }
              )
            this.vscroll.init()
        }
       }
      
    },
    blur (){
       this.isTocuh = false 
    },
    focus (){
       this.isTocuh = true 
    },
    onClick(event){
      this.KeyValue.key=event.target.innerHTML
      this.KeyValue.value=event.target.value
    },
    vDomScroll(){
      // 只有启动虚拟滚动时候生效
     if (this.isVdom) {
        this.KeyValue.key=this.vscroll.key
        this.KeyValue.value=this.vscroll.value
     }
      
    }
  },
  render (h) {
    return (
      <div class="select"  onClick={this.handleClickExpand}>
        <input style='border-radius: 3px;' value={this.KeyValue.key}  type="text" readonly="readonly" autocomplete="off" onBlur={this.blur} onFocus={this.focus} placeholder="请选择" class={[this.isTocuh ?'select_selected ':'select_select']} />
        <span class='arrowBox'>
          <span class={[this.expand ? 'arrow' : 'down']} ></span>
        </span>
        <div class='options' style={{display: this.expand ? 'none' : 'block'}}>
          <ul class='ul_ul'  id="container"  onClick={this.vDomScroll}>
              {/*	少量数据时直接使用jsx循环即可,启动虚拟滚动时自动隐藏*/}
              <div   style={{display: this.isVdom ? 'none' : 'block'}}>
                {
                  this.dataList.map((item,index)=>{
                    return <li class='ul_li' key={index} value={item.value}  onClick={this.onClick} >{item.key}</li>
                  })
                }
              </div>
          </ul>
        </div>
      </div>
    )
  }
}

</script>
<style scoped>
::-webkit-scrollbar-track-piece {
  background-color: #f8f8f8;
}
::-webkit-scrollbar {
  width: 5px;
  height: 5px;
}
::-webkit-scrollbar-thumb {
  background-color: #dddddd;
  background-clip: padding-box;
  min-height: 28px;
}
::-webkit-scrollbar-thumb:hover {
  background-color: rgba(#bbb, #bbb, #bbb, 0.3);
}
.select {
  width: 240px;
  position: relative;
  height: 40px;
}
.select_select {
  cursor: pointer;
  width: 240px;
  height: 40px; /* 40/16 */
  border: 1px solid #dcdfe6;
  color: #606266;
  outline: none;
  font-weight: 400;
  padding-right: 20px;
  padding-left: 10px;
}
.select_selected {
  cursor: pointer;
  width: 240px;
  color: #606266;
  height: 40px; /* 40/16 */
  border: 1px solid #dcdfe6;
  outline: 1px solid #409eff;
  font-weight: 400;
  padding-right: 20px;
  padding-left: 10px;
}
.arrowBox {
  cursor: pointer;
  display: block;
  position: absolute;
  right: 0px;
  top: 0px;
  width: 20px;
  height: 100%;
}
.arrow {
  position: absolute;
  width: 8px;
  height: 8px;
  right: 4px;
  top: 35%;
  border-top: 1px solid gray;
  border-left: 1px solid gray;
  transform: rotate(-135deg);
  transition: all 0.3s;
}
.down {
  position: absolute;
  width: 8px;
  height: 8px;
  right: 0px;
  top: 45%;
  border-top: 1px solid gray;
  border-left: 1px solid gray;
  transform: rotate(45deg);
  transition: all 0.3s;
}
.options {
  border-radius: 2px;
  margin-top: 10px;
  width: 260px;
  height: 280px;
  padding: 5px;
  box-shadow: 1px 1px 1px 1px #dddada;
}
ul,
li {
  padding: 0;
  margin: 0;
  list-style: none;
}
.ul_ul {
  height: 100%;
  overflow-y: scroll;
  cursor: pointer;
}
.ul_li {
  width: 100%;
  height: 40px;
  color: #606266;
  text-align: center;
  line-height: 40px;
}
.ul_li:hover {
  background-color: #f5f7fa;
}
.totalBox {
  width: 100%;
  /* background-color: pink; */
}
.listBox {
  border: 1px solid blue;
  height: 600px;
  box-sizing: content-box;
  width: 100%;
}
</style>

这部分是核心页面 这里jsx写的dom 主要看着方便。具体会分成是否开启虚拟滚动列表功能2个区别写
的,需要注意的是2个外部引入的js,index.js注意针对数据源操作和非虚拟滚动时的操作,scroll.js是针对虚拟滚动时候的操作,这里功能比较少 仅有渲染回显的双向绑定功能。

index.js

class Ref {
  constructor(data) {
    this._data = data
  }
  // 只读属性
  get data () {
    return this._data;
  }

}
let p = [] //存入数据 避免重复循环
const init = (options, select) => {
  // 格式化处理入参键值对
  // 内部固定键值对 key-value
  let result = []
  if (options.length > 0 && select) options.forEach(e => {
    result.push({
      key: e[select['key']],
      value: e[select['value']],
    })
  });

  if (result.length > 0) {
    p = new Ref(result).data
  }
  return result || []

}
// 根据value值查找出对应key值
const filter = (value) => {
  if (!value) return
  if (p && p.length > 0) {
    return p.find(e => {
      if (e.value === value) return e
    }).key
  }
}
export default {
  init,
  filter
}

scroll.js

// 抽象下拉框数据对象
const Refaa = class {
  _value = ''
  _key = ''
  constructor(op) {
    this.el = op.el
    this.liHeight = op.liHeight
    this.data = op.data
    this.list = []
    this.offsetY = 0
    this.tolHeight = this.data.length * this.liHeight //所有数据总高度
    this.lastTime = new Date().getTime()
    this.value = ''
    this.key = ''
  }
  get value () {

    // 只读属性
    return this._value
  }
  set value (value) {
    if (value) {
      this._value = value
    }

  }
  get key () {
    // 只读属性
    return this._key
  }
  set key (val) {
    if (val) {
      this._key = val
    }
  }
  init () {
    this.create()
    this.initData()
    // 初始化滚动事件
    this.el.addEventListener(
      'scroll',
      //  这里本来应该写滚动函数,又去this指向发生改变,所以直接借用滚动函数的this过来
      this.handeScroll.bind(this), //ps 这里需要用bind函数不能用call和apply,详情可参考(this指向问题2文章)
      false
    )
  }
  create () {
    // 生成dom结构
    let totalBox = document.createElement('div') // 生成总盒子 超出隐藏
    totalBox.className = 'totalBox'
    totalBox.style.height = this.tolHeight + 'px'
    let listBox = document.createElement('div') // 生成可视区域盒子
    listBox.className = 'listBox'
    totalBox.append(listBox)
    this.el.append(totalBox)
    this.totalBox = totalBox
    this.listBox = listBox
  }
  initData () {
    // 计算行数 由于固定高度为600px
    this.showNum = 280 / this.liHeight
    // this.showNum = Math.floor(600 / this.liHeight) + 4
    this.list = this.data.slice(0, this.showNum)
    this.createByList(this.list)
  }
  createByList (list) {
    this.listBox.innerText = '' // 不能缺少 每次重新渲染需要清空上次记录
    let fragment = document.createDocumentFragment()
    let li = ''
    let this_ = this
    // 生成虚拟标签每次循环都添加在虚拟标签里面 减少dom操作
    for (let i = 0; i < this.showNum; i++) {
      li = document.createElement('li')
      li.style.height = this.liHeight + 'px'
      li.style.textAlign = 'center'
      li.style.color = '#606266'
      li.style.lineHeight = this.liHeight + 'px'
      li.onmouseover = function (e) {
        this.style.background = '#f5f7fa'
      }
      li.onmouseout = function (e) {
        this.style.background = '#fff'
      }
      li.setAttribute('value', list[i].value)
      li.innerText = list[i].key
      li.addEventListener("click", function (e) {
        this_.value = e.target.value
        this_.key = e.target.innerHTML
      }, true)
      fragment.append(li)
    }
    this.listBox.append(fragment)
  }
  handeScroll (e) {
    // 滚动事件给一个延迟处理 防止重复滚动
    if (new Date().getTime() - this.lastTime > 20) {
      // 滚动事件
      let scrollTop = e.target.scrollTop //滚动距离
      let prevNum = Math.floor(scrollTop / this.liHeight) //向下取整 (展示滚动的行数)
      this.offsetY = scrollTop - (scrollTop % this.liHeight) //需要偏移的距离
      // console.log(this.offsetY, ' this.offsetY')
      // console.log(this.data, 'this.data')
      // 获取新滚动的数据进行加载渲染
      // 根据下标的变化 由于这次滚动距离scrollTop,可以得到prevNum为改变的位置 也就是下标,再和上一次滚动的位置得到一个新的结束位置下标,可以得到新展示区域的数据
      this.list = this.data.slice(prevNum, prevNum + this.showNum)
      // console.log(this.list, '    this.list')
      // 新的到的数据再次构建虚拟节点
      this.createByList(this.list)
      this.listBox.style.transform = `translateY(${this.offsetY}px)` //可视区域随之改变y距离
      this.lastTime = new Date().getTime()
    }
  }
  // 销毁
  destroy (el) {
    // 删除每次生成的dom结构
    for (let i = el.children.length - 1; i >= 0; i--) {
      el.removeChild(el.children[i]);
    }

  }
}

export default {
  Refaa
}

整体大概就是这样 具体可能性能等方面还有问题,可以自行优化啥的
这里是demo地址可以下载本地看https://gitee.com/mengjie1234/me

你可能感兴趣的:(vue.js,javascript,前端)