ant design v3 select 下拉大数据的问题
v3 的版本中没有 虚拟滚动, 对大数据会卡顿. 这里实现一个简单的分页来处理.
在后续版本(从 v4 开始)中有
virtual虚拟滚动, 可以解决这个问题.
代码的实现不区分什么框架, 这里的核心内容是 DOM 操作.
1. 实现算法说明
其实现核心在于 Element 元素的几个属性:
element.clientHeight. 只读属性. 元素的内部高度. 编码时可以理解为元素的高度. 细节可以参考 MDN 文档.element.scrollHeight. 只读属性. 元素实际上的所有内容高度. 包括可见与不可见(滚动到可视区域外部)区域. 可参考MDN 文档.element.scrollTop. 可读可写属性. 表示元素在滚动时, 超出顶部区域的距离. 可以理解为滚出去的距离. 细节参考 MDN 文档.
如果要做分页:
- 有一个固定区域. 即可视区域. 可以使用
clientHeight描述. - 有一个可滚动的容器, 数据列表放在该容器中. 并且设定加载分页数据到里面. 其高度为
scrollHeight. - 滚动, 即主动或被动修改
scrollTop的大小. 当触底时, 即scrollTop与clientHeight接近scrollHeight触发加载下一页数据的行为. 此时scrollHeight会增加 (因为数据加载了), 但是scrollToo不会变化, 因此页面不会有影响.
该算法是简化了数据加载, 使用分页处理了数据. 与虚拟滚动还是有区别. 若实现虚拟滚动, 本质加载的是
3个clientHeight的区域, 所谓滚动区域. 逻辑实现上类似于sweeper(走马灯).
对于搜索行为, 在搜索后需要更新下拉数据列表. 同时重置分页信息.

2. 一些注意事项
当下拉框滚动触底时, 需要提供一个阈值. 代码中会有下面的判断
if (scrollTop + clientHeight + refreshDistance >= scrollHeight) {
log(`下拉框滚动触发加载更多`)
...
}
如果数据已经全部加载完成, 这段代码会反复执行, 会阻塞页面中鼠标滚轮事件.
需要在加载完成时, 组织 setState(...).
3. 实现代码参考
import { Select } from 'antd'
import debug from 'debug'
const log = debug('debug: BigSelect')
/**
* 注意: 未封装所有属性与事件.
*
* 对 Antd 原有 Select 进行重构, 参数保留 Select 原有的参数.
* 追加 selectPageSize 参数, 默认值为 20.
* 追加 options 参数, 用于绑定 Select 选项.
*
* @param {Array} props.options 用于绑定 Select 选项.
* @param {String} props.options.value 选项值.
* @param {String} props.options.label 选项标签.
* @param {String} props.options.id 选项id.
*/
export class BigSelect extends React.Component {
constructor(props) {
super(props);
this.refreshDistance = 5;
this.state = {
searchText: '',
pageIndex: 1,
pageSize: 50,
optionlist: [],
renderOptions: []
}
}
searchTextHandler = (searchText) => {
log(`下拉框搜索: %s`, searchText)
this.setState({
searchText,
pageIndex: 1,
renderOptions: this.state.optionlist
.filter(opt => opt.label?.includes(searchText))
.slice(0, 1 * this.state.pageSize),
})
}
onPopupScroll = (e) => {
log(`下拉框滚动 p = %s(%s, %s, %s, %s)`,
this.state.pageIndex,
e.target.scrollTop,
e.target.clientHeight,
e.target.scrollTop + e.target.clientHeight,
e.target.scrollHeight
)
const { scrollTop, clientHeight, scrollHeight } = e.target;
if (this.state.renderOptions.length === 0) return;
if (this.state.optionlist.length === 0) return;
if (this.state.renderOptions.length === this.state.optionlist.length) return;
if (scrollTop + clientHeight + this.refreshDistance >= scrollHeight) {
log(`下拉框滚动触发加载更多`)
const _tmp = this.state.optionlist
.filter(opt => opt.label?.includes(this.state.searchText))
const _rendTmp = _tmp
.slice(0, (this.state.pageIndex + 1) * this.state.pageSize)
if (_rendTmp.length == _tmp.length) {
log(`下拉框数据已经全部加载完成`)
return;
}
this.setState({
pageIndex: this.state.pageIndex + 1,
renderOptions: _rendTmp,
})
}
}
componentDidMount() {
log(`DOM 挂载`)
}
componentDidUpdate(prevProps) {
log(`传入数据更新`)
if (prevProps.options !== this.state.optionlist) {
this.setState({
pageIndex: 1,
optionlist: prevProps.options,
renderOptions: prevProps.options.slice(0, this.state.pageSize),
})
}
}
render() {
return <Select
{...this.props}
onSearch={s => this.searchTextHandler(s)}
onPopupScroll={e => this.onPopupScroll(e)}
>
{this.state.renderOptions.map((options, i) =>
<Select.Option
key={options.id}
value={options.value}
>{options.label}</Select.Option>
)}
</Select>
}
}