快放假了没事写个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