jk's notes
  • ant design v3 select 下拉大数据的问题

ant design v3 select 下拉大数据的问题

v3 的版本中没有 虚拟滚动, 对大数据会卡顿. 这里实现一个简单的分页来处理.

在后续版本(从 v4 开始)中有 virtual 虚拟滚动, 可以解决这个问题.

代码的实现不区分什么框架, 这里的核心内容是 DOM 操作.

1. 实现算法说明

其实现核心在于 Element 元素的几个属性:

  • element.clientHeight. 只读属性. 元素的内部高度. 编码时可以理解为元素的高度. 细节可以参考 MDN 文档.
  • element.scrollHeight. 只读属性. 元素实际上的所有内容高度. 包括可见与不可见(滚动到可视区域外部)区域. 可参考MDN 文档.
  • element.scrollTop. 可读可写属性. 表示元素在滚动时, 超出顶部区域的距离. 可以理解为滚出去的距离. 细节参考 MDN 文档.

如果要做分页:

  1. 有一个固定区域. 即可视区域. 可以使用 clientHeight 描述.
  2. 有一个可滚动的容器, 数据列表放在该容器中. 并且设定加载分页数据到里面. 其高度为 scrollHeight.
  3. 滚动, 即主动或被动修改 scrollTop 的大小. 当触底时, 即 scrollTop 与 clientHeight 接近 scrollHeight 触发加载下一页数据的行为. 此时 scrollHeight 会增加 (因为数据加载了), 但是 scrollToo 不会变化, 因此页面不会有影响.

该算法是简化了数据加载, 使用分页处理了数据. 与虚拟滚动还是有区别. 若实现虚拟滚动, 本质加载的是 3 个 clientHeight 的区域, 所谓滚动区域. 逻辑实现上类似于 sweeper (走马灯).

对于搜索行为, 在搜索后需要更新下拉数据列表. 同时重置分页信息.

img

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>
  }
}
Last Updated: 11/28/25, 2:37 PM
Contributors: jk