80行代码,领你手撸一个Tree组件

前端工程师肯定对Tree组件很熟悉,那它是怎么实现的呢?本篇文章,小编就带着大家实现一个简易版本的tree组件。

市面上的Tree组件都差不多一样,我们以antd为例:

80行代码,领你手撸一个Tree组件_第1张图片
其实看到这个组件,我没多想,就直接打开了控制台,查看了这个组件的dom结构,如下图:

80行代码,领你手撸一个Tree组件_第2张图片

这么一看,着实清晰了。那接下来的问题就很清晰了:

  • 如何定义数据类型
  • 如何抽取最小单元
  • 如何维护单元状态

定义数据类型

这个我觉着树就可以,数据格式如下:


let data = [
    { 
        id: 'father1',
        name: 'father1',
        value: 'father1',
        children: [
            {
                id: 'father1-child1',
                name: 'father1-child1',
                value: 'father1-child1',
                children: [
                    { id: 'father1-child1-child1', name: 'father1-child1-child1', value: 'father1-child1-child1' },
                    { id: 'father1-child1-child2', name: 'father1-child1-child2', value: 'father1-child1-child2' },
                ]
            },
            {
                id: 'father1-child2',
                name: 'father1-child2',
                value: 'father1-child2'
            }
        ]
    },
    { id: 'father2', name: 'father2', value: 'father2' }
];

抽取最小单元

其实从下图就可以看出来,每一条数据都是一个最小单元:

80行代码,领你手撸一个Tree组件_第3张图片
具体如下:

import React, { useState } from 'react';

function Tree(props){
    let [ data, setData ] = useState([
        { 
            id: 'father1',
            name: 'father1',
            value: 'father1',
            children: [
                {
                    id: 'father1-child1',
                    name: 'father1-child1',
                    value: 'father1-child1',
                    children: [
                        { id: 'father1-child1-child1', name: 'father1-child1-child1', value: 'father1-child1-child1' },
                        { id: 'father1-child1-child2', name: 'father1-child1-child2', value: 'father1-child1-child2' },
                    ]
                },
                {
                    id: 'father1-child2',
                    name: 'father1-child2',
                    value: 'father1-child2'
                }
            ]
        },
        { id: 'father2', name: 'father2', value: 'father2' }
    ]);

    return 
{ data.map(item => { return }) }
} export default Tree;

组件也特别的好写,支持传入一个对象,将对象的name作为显示,并且,如果有children属性,那就再map递归渲染TreeNode组件,具体如下:

function TreeNode(props){
    let [ privateObj, setPrivateObj ] = useState(props.obj);
    return <>
        
{ privateObj.name }
{ privateObj.children && privateObj.children.map( item => { return } ) } }

我们再添加点样式,让所有的内容都左对齐,css如下:

.tree-node-box {
    width: 300px;
    height: 50px;
    text-align: left;
    box-sizing: border-box;
}

渲染之后,现象如下:

80行代码,领你手撸一个Tree组件_第4张图片

接下来的任务就是让Tree组件有层级感,这个功能我们可以通过padding来实现,具体就是 当前层数 * 固定值 来得到当前treeNode的偏移量。

改动如下:


function TreeNode(props){
    let [ privateObj, setPrivateObj ] = useState(props.obj);
    return <>
        <div className='tree-node-box' style={{
            paddingLeft: `${20 * (props.curLevel)}px`
        }}>
            { privateObj.name }
        </div>
        {
            privateObj.children && privateObj.children.map(
                item => {
                    return <TreeNode obj={item} curLevel={props.curLevel+1}/>
                }
            )
        }
    </>
}

function Tree(props){
    // 其余代码不变=====
    return <div className='tree-box'>
        {
            data.map(item => {
                return <TreeNode obj={item} curLevel = {0}/>
            })
        }
    </div>
}

现在我们通过层数的这个概念把父子孙之间的层级给渲染出来了。

80行代码,领你手撸一个Tree组件_第5张图片

维护单元状态

我们现在都是默认展开的,我们应该有一个开关,来控制它什么时候展开。在讲解这个功能之前,我们还需要给这个组件再增添一点样式,如下:

.tree-node-box {
    width: 300px;
    height: 50px;
    text-align: left;
    box-sizing: border-box;
    display: flex;
    justify-content: flex-start;
    align-items: center;
}
.icon {
    width: 16px;
    height: 16px;
    box-sizing: border-box;
    background-color: #fff;
    border: 1px solid #d9d9d9;
    border-radius: 2px;
    margin-right: 5px;
}

同时,我们再给TreeNode组件增加一个icon的标签,修改如下:

function TreeNode(props){
    let [ privateObj, setPrivateObj ] = useState(props.obj);
    return <>
        
{ privateObj.name }
{ privateObj.children && privateObj.children.map( item => { return } ) } }

现在样式如下:

80行代码,领你手撸一个Tree组件_第6张图片
现在我们来看一下展开收起的逻辑。想要实现这个功能,我觉着下面的几点应该是绕不过去的:

  • 展开收起肯定是组件的私有状态。
  • 这个私有状态是否跟数据有关。也就是state的初始值是否跟props相关。

显而易见,关键点在最后一个,而最后一个的解决方案是跟Tree组件的用途紧密相关的。

如果你的业务里说明,Tree组件里节点的展开收起是用户可配置的,那就是A方案;如果不支持用户配置,那就是B方案。

方案B

这2个方案我们都会讲到,首先是B方案(不支持用户配置),这个就比较简单了,组件的初始状态都是false,只有点击的时候,才会去一层一层的展开,修改如下:

function TreeNode(props){
    let [ privateObj, setPrivateObj ] = useState(props.obj);
    let [ curIsSelected, setCurInSelected ] = useState(false);  // 当前是否被选中

    // 点击当前treenode事件
    let clickCurTreeNode = () => {
        setCurInSelected(
            state => {
                return !state
            }
        );
    }

    return <>
        
{ privateObj.name }
{ curIsSelected && privateObj.children && privateObj.children.map( item => { return } ) } }

修改样式如下:

/** 其余样式不变 */
.selected-icon {
    position: relative;
    background-color: #1677ff;
}

.selected-icon::after {
    content: '';
    display: block;
    position: absolute;
    top: 50%;
    inset-inline-start: 25%;
    display: table;
    width: 8px;
    height: 8px;
    border: 2px solid #fff;
    border-top: 0;
    border-left: 0px;
    box-sizing: border-box;
    transform: rotate(45deg) translate(-50%,-50%);
}

80行代码,领你手撸一个Tree组件_第7张图片

方案A

这种方案就是支持用户控制每个节点的默认展开与隐藏,想要做到这点,我们就需要给用户一个反馈,用户的数据里必须有isSelected属性,组件才会去支持用户控制节点的展开与收起。具体的数据格式如下:

let data = [
    {
        id: 'father1',
        name: 'father1',
        value: 'father1',
        isSelected: true,
        children: [
            {
                id: 'father1-child2',
                name: 'father1-child2',
                value: 'father1-child2',
            }
        ]
    }
];

根据上面的数据配置,我们大致能够推测出来,这个结果应该是父级展开,子级不展开。

走到这里,我们还需要考虑一个事情,当前的Tree组件是否有权利改变用户的真实数据。

是否有权利改变用户的数据,这个就看具体场景而定吧。

修改如下:

function TreeNode(props){
    let [ privateObj, setPrivateObj ] = useState(props.obj);
    // 新做出的修改+++++++
    let [ curIsSelected, setCurInSelected ] = useState(props.obj.isSelected);  // 当前是否被选中
    let [ allChildIsSelected, setAllChildIsSelected ] = useState(false);  // 当前所有的子元素,是否都被选中

    let clickCurTreeNode = () => {
        setCurInSelected(
            state => {
                return !state
            }
        );
    }

    return <>
        
{ privateObj.name }
{ curIsSelected && privateObj.children && privateObj.children.map( item => { return } ) } }

当我们传入这样的数据,刷新页面后,组件是一个正确的结果:

let data = [
    { 
        id: 'father1',
        name: 'father1',
        value: 'father1',
        isSelected: true,
        children: [
            {
                id: 'father1-child2',
                name: 'father1-child2',
                value: 'father1-child2',
            }
        ]
    },
    { id: 'father2', name: 'father2', value: 'father2' }
];

效果如下:

80行代码,领你手撸一个Tree组件_第8张图片

如何实现全选 or 全不选

这个也比较好弄,在递归的时候,将当前节点的isSelected属性覆盖子组件的属性就可以了,修改如下:

function TreeNode(props){
    let [ curIsSelected, setCurInSelected ] = useState(props.obj.isSelected);  // 当前是否被选中
    return <>
        {/** 其余代码不变====== */}
        {
            curIsSelected && privateObj.children && privateObj.children.map(
                item => {
                    return 
                }
            )
        }
    
}

最终效果如下:

80行代码,领你手撸一个Tree组件_第9张图片

待完成的点

参考市面上的组件,Tree组件还应该有一个半选中状态。想要实现这个半选中,首先就要改造一下我们的curIsSelected私有状态,在这里它是一个boolean值,但是扩展性上来看,他应该是一个枚举比较好。

还有一个就是遍历,每次选中,你都要遍历当前分支(因为你始终都要反馈给上层节点),至于怎么遍历,方法有很多,欢迎大家评论区里pk。

最后

好啦,本篇到这里就结束啦,希望我的分享能够对你有帮助,如果上述讲解中出了一些错误,或者存在明显的改进点,也欢迎大家在评论区里指出,我们下期再见啦~~

你可能感兴趣的:(前端,javascript,reactjs)