这套题还不错,感兴趣的猿可以试一试:前端开发工程师
用清晰的层级结构展示信息,可展开或折叠。
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 定义树节点的属性和方法