由于业务对页面性能要求很高,如果下拉框数据很大,如果一个页面有多个下拉框,那么就导致页面很卡顿。由于elementPlus已经支持了下拉组件虚拟列表,但是所在项目仍然使用elementUI2.0,所以需要自己扩展支持下拉组件虚拟列表,以下是笔者总结的一篇关于elementUI2.0支持下拉框虚拟列表的实践方案,希望看完在项目中有所帮助。
正文开始…
在开始本文之前,笔者主要会从以下方向上去实现该业务需求
1、尝试在原有elementUI 组件上,写一个自定义指令,支持下拉虚拟列表
2、尝试使用社区成熟的虚拟列表 插件方案实现虚拟列表
前置
我们知道虚拟列表 本质上就是在可视区域内显示对应的数据,由于数据是按需加载,所以我们首先就要明白如何实现虚拟列表,具体可以参考以前写的一篇文章了解虚拟列表背后原理,轻松实现虚拟列表
快速实现页面
我们是使用vue-cli2 快速搭建了一个基本项目
我们看下实际代码
<el-form-item label="非虚拟列表-活动名称2"><el-select v-model="form.value" placeholder="请选择"><el-optionv-for="item in sourceData":key="item.value":label="item.label":value="item.value"></el-option></el-select>
</el-form-item>
对应数据就是在created 中直接生成了一组100 条数据
export default {name: 'hello-word',data() {return {sourceData: []}},created () {var arr = new Array(100).fill(1);arr.forEach((v, index) => {this.sourceData.push({value: index,label: `test_${index}`});});}
}
我们先看下左侧虚拟列表
下拉框并不是一次性渲染所有数据,而是按需获取可视区域的数据,这是如何实现的?
虚拟列表指令
主要思路就是控制下拉数据显示条数,本质就是要控制optionsData
<el-form-item label="虚拟列表-活动名称"><el-selectv-model="form.value1"placeholder="请选择"@visible-change="handleVisibleChange"v-select="{ ...selectAttrs, data: sourceData }"><el-optionv-for="item in optionsData":key="item.value":label="item.label":value="item.value"></el-option></el-select>
</el-form-item>
我们看到v-select 指令上主要有data ,selectAttrs ,data 是原数据,selectAttrs 主要是虚拟列表需要的参数
export default {name: 'hello-world',data() {return {sourceData: [], // 原始数据selectAttrs: {viewHeight: 220, // 可视区域的高度rowHeight: 30, // 当前行的默认高度startIndex: 0,endIndex: 0,callback: this.updateOptions,scrollView: null // 滚动容器}}}
}
从指令配置所需要的参数来看,主要是以下几个关键字段:
viewHeight 可视区域的高度
rowHeight 当前行的默认高度
startIndex 数据起始位置
endIndex 数据默认位置
callback 执行回调,主要是控制下拉数据
scrollView 监听滚动容器
然后我们看下指令是如何编写的
const selectDirectives = {wrap: null,fn: null,select: {inserted (el, binding, vnode) {let { data, rowHeight, startIndex, callback, filterable } = binding.value;const {componentInstance: { $children: children }} = vnode;const selectDown = children[children.length - 1];const [elScrollBar] = selectDown.$children;const [wrap] = elScrollBar.$el.childNodes;const scrollView = wrap.getElementsByClassName('el-scrollbar__view')[0];const total = data.length; // 所有数据的总条数// 设置el-scrollbar__view的高度if (filterable) {scrollView.style.height = 'auto';} else {scrollView.style.height = `${total * rowHeight}px`;}let timer = false;const fn = () => {if (timer) {return;}timer = true;const requestId = setTimeout(() => {timer = false;const scrollTop = wrap.scrollTop;// 计算当前滚动位置,获取当前开始的起始位置const currentIndex = Math.floor(scrollTop / rowHeight);// console.log(startIndex, 'startIndex222', currentIndex);// 根据滚动条获取当前索引与起始索引不相等时,将滚动的当前位置设置为起始位置if (currentIndex !== startIndex) {startIndex = Math.max(currentIndex, 0);}const paddingTop = `${startIndex * rowHeight}px`;scrollView.style.paddingTop = paddingTop;// eslint-disable-next-line standard/no-callback-literalcallback({ startIndex, scrollView });}, 100);if (!requestId) {clearTimeout(requestId);}};selectDirectives.fn = fn;selectDirectives.wrap = wrap;wrap.addEventListener('scroll', fn, false);},unbind () {selectDirectives.wrap.removeEventListener('scroll', selectDirectives.fn);}}
};
关键的几点
1、找到内容滚动容器wrap ,主要是通过componentInstance 找到下拉滚动父容器
2、设置滚动容器内部高度scrollView 【必须要设置】,不设置的话,内容数据将无法滚动显示
let { data, rowHeight, startIndex, callback } = binding.value;
const {componentInstance: { $children: children }
} = vnode;
const selectDown = children[children.length - 1];
const [elScrollBar] = selectDown.$children;
const [wrap] = elScrollBar.$el.childNodes;
const scrollView = wrap.getElementsByClassName('el-scrollbar__view')[0];
const total = data.length; // 所有数据的总条数
// 设置el-scrollbar__view的高度
scrollView.style.height = `${total * rowHeight}px`;
用一张图还原一下,为什么需要设置scrollView 的高度,以及当内部容器滚动时,我们需要给内部设置一个paddingTop ,不然显示就会有空白块
3、确定当前滚动的起始位
主要是当我们滚动容器时,根据滚动的位置确定起始位,核心代码如下
const scrollTop = wrap.scrollTop; // 计算当前滚动位置,获取当前开始的起始位置const currentIndex = Math.floor(scrollTop / rowHeight);// console.log(startIndex, 'startIndex222', currentIndex);// 根据滚动条获取当前索引与起始索引不相等时,将滚动的当前位置设置为起始位置if (currentIndex !== startIndex) {startIndex = Math.max(currentIndex, 0);}const paddingTop = `${startIndex * rowHeight}px`;scrollView.style.paddingTop = paddingTop;// eslint-disable-next-line standard/no-callback-literalcallback({ startIndex, scrollView });
4、我们看到有callback 执行回调返回出去了startIndex ,scrollView
所以从最初设计指令时,我们看到了指令的selectAttrs 上有一个callback
...
selectAttrs: {viewHeight: 250, // 可视区域的高度rowHeight: 30, // 当前行的默认高度startIndex: 0,endIndex: 0,callback: this.updateOptions,scrollView: null // 滚动容器
}
指令执行回调
主要看updateOptions
methods: {updateOptions ({ startIndex, scrollView }) {this.selectAttrs.startIndex = startIndex;this.selectAttrs.scrollView = scrollView;this.renderOptions(); },
}
我们看下renderOptions 这个方法,主要是更新下拉框数据
...
renderOptions () {let {selectAttrs: { viewHeight, rowHeight, startIndex, endIndex },sourceData} = this;const total = sourceData.length;// 可视区域的条数const limit = Math.ceil(viewHeight / rowHeight);// 设置末位索引endIndex = Math.min(startIndex + limit, total);this.selectAttrs.endIndex = endIndex;this.optionsData = sourceData.slice(startIndex, endIndex);
},
以上比较关键的一行代码就是根据回调函数中的startIndex 以及limit 确认最后的endIndex , 以下是核心关键代码
const limit = Math.ceil(viewHeight / rowHeight);// 设置末位索引
endIndex = Math.min(startIndex + limit, total);
最后我们就是根据起始位对愿数数据进行slice 操作,确认真正需要显示的数据
this.optionsData = sourceData.slice(startIndex, endIndex);
对应的页面显示
<el-selectv-model="form.value1"placeholder="请选择"@visible-change="handleVisibleChange"v-select="{ ...selectAttrs, data: sourceData }"
><el-optionv-for="item in optionsData":key="item.value":label="item.label":value="item.value"></el-option>
</el-select>
然后我们注意到,我们在下拉框下绑定了一个@visible-change="handleVisibleChange" 方法,实际上只有我们在打开下拉框时才会需要触发更新下拉,所以我们需要调用renderOptions
...
handleVisibleChange () {const {selectAttrs: { scrollView }} = this;// 当打开下拉框时,重置scrollView的paadingTop,避免白屏if (scrollView) {scrollView.style.paddingTop = '0px';}this.renderOptions();
}
但是我们注意到,这里我们重置了scrollView 的paddingTop ,因为我们在滚动时设置了paddingTop ,所以此时我们需要重置paddingTop 就是为了回显我们上次选择的内容区域
由于我们设置了内容器的高度,所以如果有设置过滤搜索,就会显示有问题,于是我们在过滤搜索时,就将高度置auto
let { data, rowHeight, startIndex, callback, filterable } = binding.value;
const {componentInstance: { $children: children }
} = vnode;const selectDown = children[children.length - 1];const [elScrollBar] = selectDown.$children;const [wrap] = elScrollBar.$el.childNodes;const scrollView = wrap.getElementsByClassName('el-scrollbar__view')[0];const total = data.length; // 所有数据的总条数// 设置el-scrollbar__view的高度
if (filterable) {scrollView.style.height = 'auto';} else {scrollView.style.height = `${total * rowHeight}px`;
}
...
挂载指令
主要是局部注册就行
// 指令
const selectDirectives = {wrap: null,fn: null,select: {inserted (el, binding, vnode) {...}
};
然后我们需要挂在在当前单文件中
export default {name: 'HelloWorld',data () {return {msg: 'Welcome to Your Vue.js App',form: {value1: '',value2: ''},sourceData: [],optionsData: [],selectAttrs: {viewHeight: 220, // 可视区域的高度rowHeight: 30, // 当前行的默认高度startIndex: 0,endIndex: 0,callback: this.updateOptions,scrollView: null, // 滚动容器filterable: true}};},directives: selectDirectives,...
}
最终结果就是下面这样了
vue-virtual-scroll-list插件实现虚拟列表
在以上例子中我们尝试用自己写的指令已经满足虚拟列表,那如果不用自己写的指令,使用社区的方案,会不会更快,更简单呢?我们考虑主要是用这个社区插件,实现起来就更简单
<template><div class="hello"><el-form ref="form" :model="form" inline><el-form-item label="活动名称"><el-selectv-model="form.value"placeholder="请选择"@visible-change="handleVisibleChange"ref="select"><virtual-list:data-key="'id'":data-sources="sourceData":data-component="optionComponent":keeps="10":extra-props="extraProps"style="max-height: 245px; overflow-y: auto;"></virtual-list></el-select></el-form-item></el-form></div>
</template>
引入vue-virtual-scroll-list
import virtualList from 'vue-virtual-scroll-list';
const optionComponent = {props: {source: {type: Object,default () {return {};}},label: String,value: String},template:'<el-option :label="source[label]" :value="source[value]"></el-option>'
};
export default {name: 'HelloWorld',components: {virtualList},data () {return {msg: 'Welcome to Your Vue.js App',form: {value: ''},optionComponent,sourceData: [],extraProps: {label: 'label',value: 'value'}};},methods: {handleVisibleChange () {const select = this.$refs.select;const child = select.$children;const [, selectDrop] = child;const [cchild] = selectDrop.$children;const [a] = cchild.$children;const [group] = a.$el.children;group.style.paddingTop = '0px';console.log(group);}},created () {var arr = new Array(100).fill(1);arr.forEach((v, index) => {this.sourceData.push({value: index,label: `test_${index}`,id: Math.random()});});}
};
我们注意到handleVisibleChange 同样是将滚动容器的paddingTop 置零了,这样保证,打开下拉框时不会白屏。
并且如果是用插件,就必须要有id ,virtual-list 上指定data-key
总结
- 主要是写了一个指令,在
elementUI 的select 组件上支持虚拟列表展示,我们在项目使用自定义指令支持下拉框的虚拟列表* 使用第三方插件vue-virtual-scroll-list 实现虚拟列表* 本文实例源码code example* 个人比较推荐社区优秀成熟的第三方库去满足我们的业务,自己虽然手写了一个指令支持虚拟列表,但是在业务时间紧凑的情况下,肯定优先使用插件,除非插件不满足我们自己的业务需求,那么只能自己造轮子了。
最后
为大家准备了一个前端资料包。包含54本,2.57G的前端相关电子书,《前端面试宝典(附答案和解析)》,难点、重点知识视频教程(全套)。 有需要的小伙伴,可以点击下方卡片领取,无偿分享
|