组件现状
ant-design-vue 版本的select
组件。本身并没有优化性能问题。这样,如果选项过多,一次渲染太多节点,页面就会炸掉。
处理方案
**(只想看最终方案请跳过此段)**而现在比较流行的性能处理方案,主要是虚拟滚动
,仅渲染列表视域内的视图。虚拟滚动
可以自己写也有成熟的插件可以使用。只是不巧的是,ant-design-vue 版本的select
组件,内部结构并不支持现在流行的虚拟滚动插件vue-virtual-scroller
,即便它隔壁的list
组件都完美支持了vue-virtual-scroller
,demo还被写在了官方示例上。
**(只想看最终方案请跳过此段)**以下就是list
组件的demo,a-list
和a-list-item
组件中间可以嵌入RecycleScroller
来管理a-list-item
的虚拟滚动。但是a-select
和a-select-option
中间并不能嵌入RecycleScroller
,a-select-option
必须是a-select
的子组件,否则会导致内置功能报错。
<a-list>
<RecycleScroller
v-infinite-scroll="handleInfiniteOnLoad"
style="height: 400px"
:items="data"
:item-size="73"
key-field="email"
:infinite-scroll-disabled="busy"
:infinite-scroll-distance="10"
>
<a-list-item slot-scope="{ item }">
<a-list-item-meta :description="item.email">
<a slot="title" :href="item.href">{{ item.name.last }}</a>
<a-avatar
slot="avatar"
src="https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png"
/>
</a-list-item-meta>
<div>Content {{ item.index }}</div>
</a-list-item>
</RecycleScroller>
<a-spin v-if="loading" class="demo-loading" />
</a-list>
**(只想看最终方案请跳过此段)**而自定义下拉框内容的APIdropdownRender
也并不能自定义下拉列表的选项,他只能在下拉框的下拉区域将a-select-option
的列表视为一个整体,在列表外添加一些类似于添加选项的额外功能按钮。
**(只想看最终方案请跳过此段)**如下图,含有滚动条在内,a-select-option
的列表是一个整体,这个地方是没有办法重写的,这个API只能再在列表外添加类似于Add item这样的按钮。
于是我们最终决定,基于antdv
的select
组件,自己实现简单的虚拟滚动视图。
现在的虚拟滚动方案一般有两种:
一种是vue-virtual-scroller
的实现方式,他完美的实现了超长列表的视图效果,包括滚动条的位置和大小,都与超长列表完全一致。
本文并没有采用这种实现方式,主要因为这种方式需要很多定制化处理,而ant-design-vue 版本的
select
比较难结合这些处理。考虑开发成本,我们选择了第二种。
第二种实现也达到了只显示滑动区域数据并无缝滚动的目的,唯一的不足是,这种方案没有实现滚动条位置的一致,即使有3万条数据,滑动的时候,滚动条还是会站到总体长度的一小半。
这主要因为该方案并没有模拟列表的长度,但也因此开发起来更加简易快速。 他不需要更加定制化的下拉框,更精细的操控。只需要简单的改变下拉框数据选项的数据,以及滚动条的位置即可。
代码实现
select.vue
组件属性
a-select(
placeholder="Please select",
:show-search="true",
show-arrow,
:options="selectOption",
@search="handleSearch"
@popupScroll="handleScroll"
@dropdownVisibleChange="handleDropdown"
)
实现逻辑
1、【限制仅渲染视域内的数据条数】
设定一个固定数量,如30条数据,最多只渲染30条数据。
2、【标记渲染数据的开始结束位置】
设定一个开始结束标记(start,end),标记渲染的数据,从第几条开始,到第几条结束。end由start得出,start默认为0.
3、【根据滚动改变开始结束位置】
选项列表滚动时,根据滚动的方向,当快滚动到底部或者顶部的时候,改变开始结束标记。(例如:向下滚动到一定位置,后面还有10条以上数据,则开始结束标记+10)
4、【调整滚动条位置】
展示出的列表数据改变时,切换开始结束标记位置时,列表的滚动位置会改变,需要手动处理滚动位置到数据变动触发的原衔接处。
上滚、上滚到顶部、下滚,位置跳动的表现都不一样
,具体处理参照代码中的计算方式。至于为什么会出现这样的情况,我目前解释不太清楚,完全是实践出的算法逻辑。如果有哪位大佬清楚麻烦分享一下!
- 以下gap代表此次滚动,数据变动的条数
- 32代表一条数据所占高度
下滚:
e.target.scrollTop -= 32 * gap;
上滚未一次滚到顶部:
// 不处理
上滚一次滚到顶部:
e.target.scrollTop += 32 * gap / 2;
5、如果不单独处理scrollTop = 0,会出现滚一半滚不动的bug。
6、处理滚动位置后,还会触发原生的滚动逻辑,若想获得最终滚动位置并记录,请记得$nextTick
。
7、处理滚动,可以使用select组件提供的popupScroll事件。处理后,需要再自定义搜索方法search。
8、demo中,itemObj是原始数据源,filterItemObj是搜索后的数据源,selectOption是最终显示出来的视域内的数据源。
具体代码
data() {
return {
start: 0,
isScrolling: false,
scrollLoc: 0,
keyword: '',
itemObj: [{
key: 'xxx',
value: 'xxx',
label: 'name'
},
...
]
};
},
computed: {
filterItemObj() {
return this.itemObj.filter(item => item.label.includes(this.keyword));
},
showLimit() {
return this.filterItemObj.length > 30 ? 30 : this.filterItemObj.length;
},
end() {
return (this.start + this.showLimit) > this.filterItemObj.length ? this.filterItemObj.length : (this.start + this.showLimit);
},
selectOption() {
return this.filterItemObj.slice(this.start, this.end);
},
},
methods: {
handleSearch(e) {
this.keyword = e;
},
async handleScroll(e) {
if (this.isScrolling) return;
this.isScrolling = true;
let scrollTop = e.target.scrollTop;
let scrollHeight = e.target.scrollHeight;
let clientHeight = e.target.clientHeight;
let scrollBottom = scrollHeight - clientHeight - scrollTop;
if (scrollTop > this.scrollLoc) {
if (scrollBottom <= 4 * 32) {
let oldStart = this.start;
this.start += 10;
this.start = this.start > this.filterItemObj.length - this.showLimit ? this.filterItemObj.length - this.showLimit : this.start;
let gap = this.start - oldStart;
e.target.scrollTop -= 32 * gap;
}
} else if (scrollTop < this.scrollLoc) {
if (scrollTop <= 4 * 32) {
let oldStart = this.start;
this.start -= 10;
this.start = this.start > 0 ? this.start : 0;
let gap = oldStart - this.start;
if (scrollTop === 0) e.target.scrollTop += 32 * gap / 2;
}
}
await this.$nextTick();
this.scrollLoc = e.target.scrollTop;
this.isScrolling = false;
},
handleDropdown(e) {
this.keyword = '';
},
}