这套题还不错,感兴趣的猿可以试一试:前端开发工程师

Element-ui Tree

用清晰的层级结构展示信息,可展开或折叠。

Tree Attributes

参数 说明 类型 可选值 默认值
data 展示的数据 Array
node-key 每个树节点用来作为唯一标识的属性,整棵树应该是唯一的 String
show-checkbox 节点是否可被选择 boolean false
default-expand-all 是否默认展开所有节点 boolean fasle

基础的树形结构展示

<template>
    <el-tree 
        :data="data" 
        :props="defaultProps" >
    </el-tree>
</template>
<script>
  export default {
    data() {
      return {
        data: [{
          label: '一级 1',
          children: [{
            label: '二级 1-1',
            children: [{
              label: '三级 1-1-1'
            }]
          }]
        }, {
          label: '一级 2',
        }],
        defaultProps: {
          children: 'children',
          label: 'label'
        }
      };
    },
  };
</script>
 

Tree 结构

实现思路

  • tree.vue 渲染树
  • tree-node.vue 渲染子树
  • tree-store.js 树的状态管理器
  • node.js 定义树节点的树形和方法

tree.vue

tree.vue 为树组件的入口

  • 接收 props 传递的数据 data
  • 根据 data 生成树节点 root
  • 根据 root 渲染树
接收 props 传递的数据 data
props: {
    // 树要展示的数据
    data: {
        type: Array,
    },
    // 树节点的key
    nodeKey: String,
    // 配置项
    props: {
        type: Object,
        default: function() {
            return {
                // 指定子树为节点某个对象的值即"children"对应值表示子节点的数据
                children: 'children',
                // 指定节点标签为节点对象的某个属性的值
                label: 'label',
            }
        }
    },
}
 
根据 data 生成树节点 root
created() {
    // 给子树判断父组件是否为树
    this.isTree = true;

    // 创建树的store
    this.store = new TreeStore({
        key: this.nodeKey,
        data: this.data,
        props: this.props,
    });

    // 从树根开始
    this.root = this.store.root;
},
 

根据 root 渲染树

<template>
    <div
        class="y-tree">
        <!-- 子树如何渲染?循环生成?多层嵌套? -->
        <y-tree-node
            v-for="(child) in root.childNodes"
            :node="child"
            :show-checkbox="showCheckbox"
            :key="getNodeKey(child)"
        >
        </y-tree-node>
    </div>
</template>
 

tree-node.vue

tree-node.vue 渲染子树

如何渲染子树的子树...循环嵌套渲染子树?我原来的想法是循环嵌套就是顺下去,往下走;实际上,循环嵌套式一个环,往下走完还要往回走

  • 渲染该节点展示的内容
  • 渲染该节点的子树
渲染子树
<template>
    <div
        class="y-tree-node"
        :aria-expanded="expanded"
        @click.stop="handleClick"
    >
        <!-- 1.渲染树节点
            节点内容的展示,el-tree节点主要分四部分(展开图标展示,多选框,加载中图标,节点内容展示)
            动态计算偏移量:(node.level - 1) * treeC.indent + 'px', 形成阶梯式子树效果
        -->
        <div
            class="y-tree-node__content"
            :style="{'padding-left': (node.level - 1) * treeC.indent + 'px'}"
        >
            <!-- 多选框 
                @click.native.stop 阻止事件冒泡
                事件修饰符-官方文档:https://cn.vuejs.org/v2/guide/events.html
            -->
            <y-checkbox
                v-if="showCheckbox"
                v-model="node.checked"
                @click.native.stop
            >
            </y-checkbox>
            <!-- 内容 -->
            <node-content :node="node"></node-content>
        </div>
        <!-- 2.渲染该节点的子树
            子树如何渲染?
            组件YCollapseTransition是一个函数式组件
            官方文档:https://cn.vuejs.org/v2/guide/render-function.html
            这里为什么要用函数式组件(无状态、无实例)来包装树节点组件从而实现子树的渲染?为什么不直接使用? -- 子树的展开和收缩效果
         -->
         <y-collapse-transition>
             <!-- v-if="node.expanded && node.childNodes.length"
                v-if: 动态的控制DOM元素的添加和删除
                v-show: 同css的display来控制元素的显示和隐藏
              -->
            <div
                v-if="childNodeRendered"
                v-show="expanded"
                class="y-tree-node__children"
                :aria-expanded="expanded"
            >
                <y-tree-node
                    v-for="(child) in node.childNodes"
                    :key="getNodeKey(child)"
                    :node="child"
                    :show-checkbox="showCheckbox"
                >
                </y-tree-node>
            </div>
         </y-collapse-transition>
    </div>
</template>
 

tree-store.js

tree-store 树的状态管理器,生成树节点集

import Node from './node';

export default class TreeStore {
    constructor(options) {

        // 赋值初始化:options是对象,遍历使用for...in...
        for(let option in options) {
            if(options.hasOwnProperty(option)) {
                this[option] = options[option];
            }
        }
        
        /**
         * 实例化根节点Node
         * 根节点实例化
         * 由根节点开始生成树
         * 根节点->根节点的childNodes->...
         */
        this.root = new Node({
            data: this.data,
            store: this,
        });
    }
} 
 

node.js

node.js 树节点的属性和方法, 每个节点都有的,保证节点的独立性

import objectAssign from '../../../../src/utils/merge';
import {
    markNodeData,
} from './utils';

/**
 * getPropertyFromData(this, 'children')
 * node.store为tree-store中的this
 * node.store.children: 函数 | 字符串 | undefined
 * 从node.data中获取prop对应的值
 * 
 * store.props 是树的配置项
 * 
 * @param {*} node 
 * @param {*} prop 
 */
const getPropertyFromData = function(node, prop) {
    const props = node.store.props;

    const data = node.data || {};

    const config = props && props[prop];
    // console.log('888', props, config, data[config]);

    if(typeof config === 'function') {
        return config(data, node);
    } else if (typeof config === 'string') {
        return data[config];
    } else if (typeof config === 'undefined') {
        const dataProp = data[prop];
        // console.log('children', dataProp)
        return dataProp === undefined ? '' : dataProp;
    }
}

// 树节点的id
let nodeIdSeed = 0;

export default class Node {
    constructor(options) {
        this.id = nodeIdSeed++;

        // 节点data
        this.data = null;

        // 是否选中,默认false:取消选中(true:选中)
        this.checked = false;

        // 半选中,默认false
        this.indeterminate = false;

        // 父亲节点
        this.parent = null;

        // 是否展开
        this.expanded = false;
        
        // 是否是当前节点
        this.isCurrent = false;

        // 赋初值
        for(let option in options) {
            if(options.hasOwnProperty(option)) {
                this[option] = options[option];
            }
        }

        // internal
        // 该节点的层级,默认为0
        this.level = 0;
        // 该节点的子节点
        this.childNodes = [];

        // 计算层级 根节点层级为0
        if(this.parent) {
            this.level = this.parent.level + 1;
        }

        const store = this.store;
        if(!store) {
            throw new Error('[Node]store is required!');
        }

        // 构建子树
        this.setData(this.data);

        // 设置节点的展开属性
        // console.log('store', this.store.defaultExpandAll);
        if(store.defaultExpandAll) {
            this.expanded = true;
        }

        // 节点注册,为什么会在tree-store中呢?为什么要注册?
        // store.registerNode(this);

        // console.log('Node', this, options);
    }

    /**
     * 通过 node.label 调用(即执行get方法)
     */
    get label() {
        return getPropertyFromData(this, 'label');
    }

    /**
     * A instanceof B:A是否是B的实例
     * 设置该节点的data和childNodes
     * 根节点下的data是一个数组,其子节点便是根据此生成的
     * @param {*} data 
     */
    setData(data) {
        // console.log('setData', Array.isArray(data), data instanceof Array);
        
        // 如果data不是数组即非根节点,则需要给节点标记id
        if(!Array.isArray(data)) {
            markNodeData(this, data);
        }

        this.data = data;
        this.childNodes = [];

        let children;
        // 如果该节点的层级为0,且该data为数组类型
        // 根节点下的data是一个数组,其子节点便是根据此生成的
        if(this.level === 0 && this.data instanceof Array) {
            children = this.data;
        } else {
            // 非根节点,看其children字段是否还存在
            children = getPropertyFromData(this, 'children') || [];
        }

        // 子节点的生成
        for(let i = 0, j = children.length; i < j; i++) {
            this.insertChild({data: children[i]});
        }

        // console.log('ndoe', this);
    }

    /**
     * 插入子节点childNodes
     * @param {*} child 
     * @param {*} index 
     */
    insertChild(child, index) {
        // console.log('insertChild', child, child instanceof Node);

        // 如果child不是Node的实例对象
        if(!(child instanceof Node)) {
            // 将后面的对象值添加到child
            objectAssign(child, {
                parent: this,
                store: this.store,
            });

            // 创建child节点
            child = new Node(child);
            // console.log('chi', child);
        }

        child.level = this.level + 1;
        // console.log('ch', child, index);

        /**
         * typeof index !== 'undefined'
         * index !== undefined
         * 
         * 将child插入到childNodes
         */
        if(typeof index === 'undefined' || index < 0) {
            // console.log(typeof index !== 'undefined')
            this.childNodes.push(child);
        } else {
            // console.log(index, index === undefined)
            this.childNodes.splice(index, 0, child)
        }
    }

    /**
     * 子树收缩
     * 设置展开属性
     * node.expanded = false
     */
    collapse() {
        this.expanded = false;
        // console.log('collapse', this, this.expanded);
    }

    /**
     * 展开子树
     * 设置节点的展开属性
     * node.expanded = true
     * 
     * 注意:树上的每个节点都具有展开和伸缩子树的方法,而不是将这两个方法共享
     * 保证了树节点的独立性质
     */
    expand() {
        // console.log('展开子树', this);
        this.expanded = true;
    }
}
 

总结

实现思路

  • tree.vue 渲染树
  • tree-node.vue 渲染子树
  • tree-store.js 树的状态管理器
  • node.js 定义树节点的属性和方法