内容简介:antd 的 Select 组件不支持大数据量的下拉列表渲染,下拉列表数量太多会出现性能问题, SuperSelect 基于 antd 封装实现,替换原组件下拉列表,只渲染几十条列表数据,随下拉列表滚动动态刷新可视区列表状态,实现大数据量列表高性能渲染。基本使用同 antd Select,只是使用 SuperSelect 代替 Select大佬们有啥更好的做法或建议请多多指教
antd 的 Select 组件不支持大数据量的下拉列表渲染,下拉列表数量太多会出现性能问题, SuperSelect 基于 antd 封装实现,替换原组件下拉列表,只渲染几十条列表数据,随下拉列表滚动动态刷新可视区列表状态,实现大数据量列表高性能渲染。
-
特性
- 基于 antd Select 组件,不修改组件用法
- 替换 antd Select 组件的下拉列表部分实现动态渲染列表
- 初步测试 10w 条数据不卡顿
-
实现方案
dropdownRender
在线地址
使用
基本使用同 antd Select,只是使用 SuperSelect 代替 Select
import SuperSelect from 'components/SuperSelect'; import { Select } from 'antd'; const Option = Select.Option; const Example = () => { const children = []; for (let i = 0; i < 100000; i++) { children.push( <Option value={i + ''} key={i}> {i} </Option> ); } return ( <SuperSelect showSearch // mode="multiple" // onChange={onChange} // onSearch={onSearch} // onSelect={onSelect} > {children} </SuperSelect> ); }; 复制代码
问题
-
多选模式选择后鼠标点击输入框中删除等图标时不能直接 hover 时获取焦点直接删除,需要点击两次
Warning: the children of `Select` should be `Select.Option` or `Select.OptGroup`, instead of `li` 复制代码
- 重写的下拉菜单没有 isSelectOption(rc-select 源码判断下拉列表元素)属性,控制台会有 warning 提示下拉列表元素不符合 Select 组件要求
大佬们有啥更好的做法或建议请多多指教
附上代码
import React, { PureComponent, Fragment } from 'react'; import { Select } from 'antd'; // 页面实际渲染的下拉菜单数量,实际为 2 * ITEM_ELEMENT_NUMBER const ITEM_ELEMENT_NUMBER = 20; // Select size 配置 const ITEM_HEIGHT_CFG = { small: 24, large: 40, default: 32, }; class Wrap extends PureComponent { state = { list: this.props.list, allHeight: this.props.allHeight, }; reactList = (list, allHeight) => this.setState({ list, allHeight }); render() { const { list } = this.state; const { notFoundContent } = this.props; // 搜索下拉列表为空时显示 no data const noDataEle = ( <li role="option" unselectable="on" className="ant-select-dropdown-menu-item ant-select-dropdown-menu-item-disabled" aria-disabled="true" aria-selected="false" > <div className="ant-empty ant-empty-normal ant-empty-small"> <div className="ant-empty-image"> <img alt="No Data" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjQiIGhlaWdodD0iNDEiIHZpZXdCb3g9IjAgMCA2NCA0MSIgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAxKSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj4KICAgIDxlbGxpcHNlIGZpbGw9IiNGNUY1RjUiIGN4PSIzMiIgY3k9IjMzIiByeD0iMzIiIHJ5PSI3Ii8+CiAgICA8ZyBmaWxsLXJ1bGU9Im5vbnplcm8iIHN0cm9rZT0iI0Q5RDlEOSI+CiAgICAgIDxwYXRoIGQ9Ik01NSAxMi43Nkw0NC44NTQgMS4yNThDNDQuMzY3LjQ3NCA0My42NTYgMCA0Mi45MDcgMEgyMS4wOTNjLS43NDkgMC0xLjQ2LjQ3NC0xLjk0NyAxLjI1N0w5IDEyLjc2MVYyMmg0NnYtOS4yNHoiLz4KICAgICAgPHBhdGggZD0iTTQxLjYxMyAxNS45MzFjMC0xLjYwNS45OTQtMi45MyAyLjIyNy0yLjkzMUg1NXYxOC4xMzdDNTUgMzMuMjYgNTMuNjggMzUgNTIuMDUgMzVoLTQwLjFDMTAuMzIgMzUgOSAzMy4yNTkgOSAzMS4xMzdWMTNoMTEuMTZjMS4yMzMgMCAyLjIyNyAxLjMyMyAyLjIyNyAyLjkyOHYuMDIyYzAgMS42MDUgMS4wMDUgMi45MDEgMi4yMzcgMi45MDFoMTQuNzUyYzEuMjMyIDAgMi4yMzctMS4zMDggMi4yMzctMi45MTN2LS4wMDd6IiBmaWxsPSIjRkFGQUZBIi8+CiAgICA8L2c+CiAgPC9nPgo8L3N2Zz4K" /> </div> <p className="ant-empty-description"> {notFoundContent || '没有匹配到数据'} </p> </div> </li> ); return ( <div style={{ overflow: 'hidden', height: this.state.allHeight }}> <ul role="listbox" className="ant-select-dropdown-menu ant-select-dropdown-menu-root ant-select-dropdown-menu-vertical" style={{ height: this.state.allHeight, maxHeight: this.state.allHeight, overflow: 'hidden', }} tabIndex="0" > {list.length > 0 ? list : noDataEle} </ul> </div> ); } } export default class SuperSelect extends PureComponent { constructor(props) { super(props); const { mode, defaultValue, value } = props; this.isMultiple = ['tags', 'multiple'].includes(mode); // 设置默认 value let defaultV = this.isMultiple ? [] : ''; defaultV = value || defaultValue || defaultV; this.state = { children: props.children || [], filterChildren: null, value: defaultV, }; // 下拉菜单项行高 this.ITEM_HEIGHT = ITEM_HEIGHT_CFG[props.size || 'default']; // 可视区 dom 高度 this.visibleDomHeight = this.ITEM_HEIGHT * ITEM_ELEMENT_NUMBER; // 滚动时重新渲染的 scrollTop 判断值,大于 reactDelta 则刷新下拉列表 this.reactDelta = (this.visibleDomHeight * 2) / 3; // 是否拖动滚动条快速滚动状态 this.isStopReact = false; // 上一次滚动的 scrollTop 值 this.prevScrollTop = 0; this.scrollTop = 0; } componentDidUpdate(prevProps, prevStates) { if (prevProps.children !== this.props.children) { const { mode, defaultValue, value } = this.props; this.isMultiple = ['tags', 'multiple'].includes(mode); // 更新时设置默认 value let defaultV = this.isMultiple ? [] : ''; defaultV = value || defaultValue || defaultV; this.setState({ children: this.props.children || [], filterChildren: null, value: defaultV, }); } } getItemStyle = i => ({ position: 'absolute', top: this.ITEM_HEIGHT * i, width: '100%', height: this.ITEM_HEIGHT, }); addEvent = () => { this.scrollEle = document.querySelector('.my-select'); // 下拉菜单未展开时元素不存在 if (!this.scrollEle) return; this.scrollEle.addEventListener('scroll', this.onScroll, false); }; onScroll = () => this.throttleByHeight(this.onScrollReal); onScrollReal = () => { this.allList = this.getUseChildrenList(); this.showList = this.getVisibleOptions(); this.prevScrollTop = this.scrollTop; // 重新渲染列表组件 Wrap let allHeight = this.allList.length * this.ITEM_HEIGHT || 100; this.wrap.reactList(this.showList, allHeight); }; throttleByHeight = () => { this.scrollTop = this.scrollEle.scrollTop; // 滚动的高度 let delta = this.prevScrollTop - this.scrollTop; delta = delta < 0 ? 0 - delta : delta; // TODO: 边界条件优化, 滚动约 2/3 可视区 dom 高度时刷新 dom delta > this.reactDelta && this.onScrollReal(); }; // 列表可展示所有 children getUseChildrenList = () => this.state.filterChildren || this.state.children; getStartAndEndIndex = () => { // 滚动后显示在列表可视区中的第一个 item 的 index const showIndex = Number( (this.scrollTop / this.ITEM_HEIGHT).toFixed(0) ); const startIndex = showIndex - ITEM_ELEMENT_NUMBER < 0 ? 0 : showIndex - ITEM_ELEMENT_NUMBER / 2; const endIndex = showIndex + ITEM_ELEMENT_NUMBER; return { startIndex, endIndex }; }; getVisibleList = () => { // 搜索时使用过滤后的列表 const { startIndex, endIndex } = this.getStartAndEndIndex(); // 渲染的 list return this.allList.slice(startIndex, endIndex); }; getVisibleOptions = () => { const visibleList = this.getVisibleList(); const { startIndex } = this.getStartAndEndIndex(); // 显示中的列表元素添加相对定位样式 return visibleList.map((item, i) => { let props = { ...item.props }; const text = props.children; const realIndex = startIndex + Number(i); const key = props.key || realIndex; const { value } = this.state; const valiValue = text || props.value; const isSelected = value && value.includes ? value.includes(valiValue) : value == valiValue; const classes = `ant-select-dropdown-menu-item ${ isSelected ? 'ant-select-dropdown-menu-item-selected' : '' }`; // antd 原素,下拉列表项右侧 √ icon const selectIcon = ( <i aria-label="icon: check" className="anticon anticon-check ant-select-selected-icon" > <svg viewBox="64 64 896 896" className="" data-icon="check" width="1em" height="1em" fill="currentColor" aria-hidden="true" focusable="false" > <path d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z" /> </svg> </i> ); props._childrentext = text; return ( <li className={classes} key={key} onMouseDown={() => this.onClick(props, item)} {...props} style={this.getItemStyle(realIndex)} > {text} {/* 多选项选中状态 √ 图标 */} {this.isMultiple ? selectIcon : null} </li> ); }); }; render() { let { children, dropdownStyle, optionLabelProp, notFoundContent, ...props } = this.props; this.allList = this.getUseChildrenList(); this.showList = this.getVisibleOptions(); let allHeight = this.allList.length * this.ITEM_HEIGHT || 100; dropdownStyle = { maxHeight: '250px', ...dropdownStyle, overflow: 'auto', position: 'relative', }; const { value } = this.state; // 判断处于 antd Form 中时不自动设置 value let _props = { ...props }; // 先删除 value,再手动赋值,防止空 value 影响 placeholder delete _props.value; if (!this.props['data-__field'] && value && value.length > 0) { _props.value = value; } // 设置显示在输入框的文本,替换 children 为自定义 childrentext,默认 children 会包含 √ icon optionLabelProp = optionLabelProp ? optionLabelProp : '_childrentext'; optionLabelProp = optionLabelProp === 'children' ? '_childrentext' : optionLabelProp; return ( <Select {..._props} onSearch={this.onSearch} onChange={this.onChange} onSelect={this.onSelect} dropdownClassName="my-select" optionLabelProp={optionLabelProp} dropdownStyle={dropdownStyle} onDropdownVisibleChange={this.setSuperDrowDownMenu} ref={ele => (this.select = ele)} dropdownRender={menu => ( <Fragment> <Wrap ref={ele => (this.wrap = ele)} allHeight={allHeight} list={this.showList} notFoundContent={notFoundContent} /> </Fragment> )} > {this.showList} </Select> ); } // 须使用 setTimeout 确保在 dom 加载完成之后添加事件 setSuperDrowDownMenu = () => { this.allList = this.getUseChildrenList(); this.allList = this.getUseChildrenList(); if (!this.eventTimer) { this.eventTimer = setTimeout(() => this.addEvent(), 0); } else { let allHeight = this.allList.length * this.ITEM_HEIGHT || 100; // 下拉列表单独重新渲染 this.wrap && this.wrap.reactList(this.showList, allHeight); } }; /** * 替换了 antd Select 的下拉列表,手动实现下拉列表项的点击事件, * 绑定原组件的各项事件回调 * itemProps: li react 元素的 props * item: li 元素 */ onClick = (itemProps, item) => { let { value } = itemProps; const { onDeselect } = this.props; let newValue = this.state.value || []; let option = item; // 多选 if (this.isMultiple) { newValue = [...newValue]; // 点击选中项取消选中操作 if (newValue.includes(value)) { newValue = newValue.filter(i => i !== value); onDeselect && onDeselect(value, item); } else { newValue.push(value); } // 获取原 onChange 函数第二个参数 options,react 元素数组 option = this.state.children.filter(i => newValue.includes(i.props.value) ); } else { newValue = value; } // 多选模式点击选择后下拉框持续显示 this.isMultiple && this.focusSelect(); this.onChange(newValue, option); this.onSelect(newValue, option); }; // 非 antd select 定义元素点击后会失去焦点,手动再次获取焦点防止多选时自动关闭 focusSelect = () => setTimeout(() => this.select.focus(), 0); // 绑定 onSelect 事件 onSelect = (v, opt) => { const { onSelect } = this.props; onSelect && onSelect(v, opt); }; onChange = (value, opt) => { // 删除选中项时保持展开下拉列表状态 if (Array.isArray(value) && value.length < this.state.value.length) { this.focusSelect(); } const { showSearch, onChange, autoClearSearchValue } = this.props; if (showSearch || this.isMultiple) { // 搜索模式下选择后是否需要重置搜索状态 if (autoClearSearchValue !== false) { this.setState({ filterChildren: null }, () => { // 搜索成功后重新设置列表的总高度 this.setSuperDrowDownMenu(); }); } } this.setState({ value }); onChange && onChange(value, opt); }; onSearch = v => { let { showSearch, onSearch, filterOption, children } = this.props; if (showSearch && filterOption !== false) { // 须根据 filterOption(如有该自定义函数)手动 filter 搜索匹配的列表 let filterChildren = null; if (typeof filterOption === 'function') { filterChildren = children.filter(item => filterOption(v, item)); } else if (filterOption === undefined) { filterChildren = children.filter(item => this.filterOption(v, item) ); } // 设置下拉列表显示数据 this.setState( { filterChildren: v === '' ? null : filterChildren }, () => { // 搜索成功后需要重新设置列表的总高度 this.setSuperDrowDownMenu(); } ); } onSearch && onSearch(v); }; filterOption = (v, option) => { // 自定义过滤对应的 option 属性配置 const filterProps = this.props.optionFilterProp || 'value'; return `${option.props[filterProps]}`.indexOf(v) >= 0; }; componentWillUnmount() { this.removeEvent(); } removeEvent = () => { if (!this.scrollEle) return; this.scrollEle.removeEventListener('scroll', this.onScroll, false); }; } 复制代码
以上所述就是小编给大家介绍的《支持大数据渲染下拉列表组件开发 SuperSelect(基于antd Select)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
付费:互联网知识经济的兴起
方军 / 机械工业出版社 / 2017-6-1 / CNY 59.00
关于互联网知识付费的首部作品 知识工作正在被重塑,知识经济正在开启互联网时代下半场 为你展现互联网知识经济全景大图,解读新物种的前世今生 内容简介 一个产业解读 三个分析工具 一组知识卡片 书是最早的知识载体,已有2000多年的付费历史,随着移动互联网的普及,新的知识经 济在今天爆发,知识的创造者和传播者从书后走到了书前,互联网知识经济正在拉开帷幕。知识的......一起来看看 《付费:互联网知识经济的兴起》 这本书的介绍吧!