支持大数据渲染下拉列表组件开发 SuperSelect(基于antd Select)

栏目: 前端 · 发布时间: 6年前

内容简介:antd 的 Select 组件不支持大数据量的下拉列表渲染,下拉列表数量太多会出现性能问题, SuperSelect 基于 antd 封装实现,替换原组件下拉列表,只渲染几十条列表数据,随下拉列表滚动动态刷新可视区列表状态,实现大数据量列表高性能渲染。基本使用同 antd Select,只是使用 SuperSelect 代替 Select大佬们有啥更好的做法或建议请多多指教

antd 的 Select 组件不支持大数据量的下拉列表渲染,下拉列表数量太多会出现性能问题, SuperSelect 基于 antd 封装实现,替换原组件下拉列表,只渲染几十条列表数据,随下拉列表滚动动态刷新可视区列表状态,实现大数据量列表高性能渲染。

  • 特性

    1. 基于 antd Select 组件,不修改组件用法
    2. 替换 antd Select 组件的下拉列表部分实现动态渲染列表
    3. 初步测试 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)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

REST in Practice

REST in Practice

Jim Webber、Savas Parastatidis、Ian Robinson / O'Reilly Media / 2010-9-24 / USD 44.99

Why don't typical enterprise projects go as smoothly as projects you develop for the Web? Does the REST architectural style really present a viable alternative for building distributed systems and ent......一起来看看 《REST in Practice》 这本书的介绍吧!

SHA 加密
SHA 加密

SHA 加密工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具