elementUI-cascader-上万级别大数据量列表

2,715 阅读2分钟

效果图:

github项目地址,地址

可以点击链接,直接体验。

首先,想了解cascader组件的整体结构的,可以先看这篇文章

由于使用了vue-virtual-scroll-list这个第三方组件,所以建议各位看官先去了解一个这个组件,地址

实现步骤:

  1. 在cascader-menu中添加vue-virtual-scroll-list。
  2. 编写一个vue-virtual-scroll-list需要的date-component组件。
  3. 在cascader-panel中改写自动滚动的方法。

由于组件上的列表即cascader-node集合是在cascader-menu中渲染的,所以我们在cascader-menu中添加vue-virtual-scroll-list组件。

cascader-menu中渲染cascader-node集合的原来代码:

renderNodeList(h) {      
    const { menuId } = this;      
    const { isHoverMenu } = this.panel;      
    const events = { on: {} };      
    if (isHoverMenu) {        
        events.on.expand = this.handleExpand;      
    }      
    const nodes = this.nodes.map((node, index) => {       
        const { hasChildren } = node;        
        return (          
            <cascader-node              
                key={ node.uid }           
                node={ node }           
                node-id={ `${menuId}-${index}` }            
                aria-haspopup={ hasChildren }            
                aria-owns = { hasChildren ? menuId : null }            
                { ...events }></cascader-node>        
        );      
    });      
    return [        
        ...nodes,        
        isHoverMenu ? <svg ref='hoverZone' class='el-cascader-menu__hover-zone'></svg> : null      
    ];    
}

我们通过配置config.virtualScroll: true来决定是否使用虚拟滚动,添加完vue-virtual-scroll-list之后的代码是:

renderNodeList(h) {      
    const { menuId, nodes } = this; 
    const { isHoverMenu, config } = this.panel;      
    const events = { on: {} };      
    if (isHoverMenu) {        
        events.on.expand = this.handleExpand;     
    }      
    this.virtualListProps.menuId = menuId;      
    const nodeItems = nodes.map((node, index) => {        
        const { hasChildren } = node;       
        return (          
            <cascader-node           
                key={ node.uid }            
                node={ node }            
                node-id={ `${menuId}-${index}` }            
                aria-haspopup={ hasChildren }            
                aria-owns = { hasChildren ? menuId : null }            
                { ...events }></cascader-node>        
        );      
    });      
    return [        
        config.virtualScroll ? <virtual-list ref="virtualList" class="el-cascader-menu__virtual-list" data-key="uid" data-sources={nodes} extra-props={this.virtualListProps} data-component={virtualListItem}>        
        </virtual-list> : [...nodeItems],        
        isHoverMenu ? <svg ref='hoverZone' class='el-cascader-menu__hover-zone'></svg> : null      
    ];    
}

我们了解一个vue-virtual-scroll-list组件(即上图代码中的virtual-list组件)的使用方式:

  1. data-sources:列表的数据源,比如[ { label: 1, value: 1 }, { label: 2, value: 2} ]
  2. data-key:列表li的每个key值,可以默认为'uid',这个是组件自动添加的。
  3. data-component:vue-virtual-scroll-list对于列表的每个item都用这个属性传入的组件来渲染,即传入列表的每个item所属的组件。
  4. extra-props:额外可传入的参数。

由于vue-virtual-scroll-list需要一个item组件,所以我们就创建一个virtualListItem组件,代码如下:

<script>  
import CascaderNode from './cascader-node.vue';  
export default {    
    name: 'ElCascaderVirtualScrollItem',    
    components: {CascaderNode},   
    props: {      
        index: { // index of current item        
            type: Number      
        },     
        source: { // here is: {uid: 'unique_1', text: 'abc'}       
            type: Object,      
            default() {          
                return {};        
            }      
        },      
        menuId: {       
            type: String,        
            default() {          
                return '';        
            }      
        }    
    },   
    render(h) {      
        const { source, menuId, index } = this;      
        return (        
            <cascader-node          
                key={ source.id }          
                node={ source }          
                node-id={ `${menuId}-${index}` }          
                aria-haspopup={source.hasChildren }         
                aria-owns={source.hasChildren ? menuId : null }        
            ></cascader-node>      
        );    
    }  
};</script>

其实上图代码就是简单的用个组件套了cascader-node。

然后在cascader-menu的渲染函数做如下修改:

return (      
    config.virtualScroll ? <div class="el-cascader-menu">        
        { isEmpty ? this.renderEmptyText() : this.renderNodeList(h) }      
    </div> : 
    <el-scrollbar        
        tag="ul"        
        role="menu"        
        id={ menuId }        
        class="el-cascader-menu"        
        wrap-class="el-cascader-menu__wrap"        
        view-class={{          
            'el-cascader-menu__list': true,          
            'is-empty': isEmpty        
        }}        
        { ...events }>        
        { isEmpty ? this.renderEmptyText(h) : this.renderNodeList(h) }      
    </el-scrollbar>    
);

对于cascader-node组件,我们先给它添加一个inActivePath属性,以及在合适的地方更新inActivePath。

我来解释一下inActivePath代表什么,请看下图:

当我们点击选择了任何一个cascader-node组件时,那么activePath就会被更新,比如此刻activePath = 东南-江苏-南京。

而inActivePath就是用来记录该cascader-node是否是在activePath中,比如东南的inActivePath = true,而西北的inActivePath = false。

而更新cascader-node的inActivePath的时刻就在下图代码:

computed: {
    inActivePath() {        
        let inActivePath = this.isInPath(this.panel.activePath);
        // 此处更新        
        this.node.inActivePath = inActivePath;        
        return inActivePath;      
    }
}

我们先看一段cascader-panel的代码:

// 该方法是:当点击cascader后,出现下拉框的时候触发,用于自动将下拉框自动滚动到合适的地方。
scrollIntoView() {      
    if (this.$isServer) return;      
    const menus = this.$refs.menu || [];
    // 遍历所有的cascader-menu     
    menus.forEach(menu => {          
        const menuElement = menu.$el;          
        if (menuElement) {
            // 查询cascader-menu中处于is-active或者is-active-path的节点
            // 即处于activePath中或者(activePath为空时,第一个被选中的)cascader-node            
            const container = menuElement.querySelector('.el-scrollbar__wrap');            
            const activeNode = menuElement.querySelector('.el-cascader-node.is-active') ||              
                menuElement.querySelector('.el-cascader-node.in-active-path');
            // 将cascader-menu自动滚动到上述代码所找到的cascader-node节点。            
            scrollIntoView(container, activeNode);          
        }      
    });    
},

由于我们用了vue-virtual-scroll-list,所有这方法的逻辑我们得自己写一个。

scrollIntoView() {      
    if (this.$isServer) return;      
    const menus = this.$refs.menu || [];
    // 遍历所有的menu      
    menus.forEach(menu => {
        // 当使用了虚拟滚动        
        if (this.config.virtualScroll) {          
            let currentNodeIndex = -1;
            // 先找到当前cascader-menu中的第一个inActivePath:true的cascader-node          
            menu.nodes.find((item, index) => {            
                let flag = item.inActivePath;            
                flag && (currentNodeIndex = index);            
                return flag;        
            });          
            if (currentNodeIndex !== -1) {
                // 如果确实有inActivePath的节点,则用vue-virtual-scroll-list的方法,直接滚动到改节点。            
                menu.$refs.virtualList && menu.$refs.virtualList.scrollToIndex(currentNodeIndex);          
            } else {
                // 如果没找到inActivePath的节点,则找到第一个checked:true的节点。然后滚动到改节点            
                menu.nodes.find((item, index) => {              
                    let flag = item.checked || item.indeterminate;              
                    flag && (currentNodeIndex = index);              
                    return flag;            
                });            
                menu.$refs.virtualList && currentNodeIndex === -1 ? menu.$refs.virtualList.reset() : menu.$refs.virtualList.scrollToIndex(currentNodeIndex);          
            }        
        } else {
            // 如果不使用虚拟          
            const menuElement = menu.$el;          
            if (menuElement) {            
                const container = menuElement.querySelector('.el-scrollbar__wrap');           
                const activeNode = menuElement.querySelector('.el-cascader-node.is-active') ||              
                menuElement.querySelector('.el-cascader-node.in-active-path');            
                scrollIntoView(container, activeNode);          
            }        
        }      
    });    
},