从0到1,手把手带你用 vue 实现最简单的树状控件
很多初学者可能觉得实现一个树结构的控件很难, 觉得树状结构很复杂,不好处理
实际上树状控件只需要你对递归、数组有基本的掌握,就可以很轻松的实现出来了
1. 数据结构
对于数据结构,其实就是简单的树状结构,每一个节点如果有子节点,那么就放入到该节点的 children 列表中
const list = reactive([
{
id: 1,
name: 1,
// 子节点
children: [
{
id: 2,
name: 2,
// 子节点
children: [
{
id: 3,
name: 3
}
]
},
{
id: 4,
name: 4
}
]
},
{
id: 5,
name: 5
}
]);
需要注意的是,后端有可能只会返回给我们平铺的数据结构,类似于
[
{id: 1, name: 1},
{id: 2, name: 2, parentId: 1},
{id: 4, name: 4, parentId: 1},
{id: 3, name: 3, parentId: 2},
{id: 5, name: 5}
]
这种就需要我们对数据做处理,将数据转换为树状的结构,方便我们后续渲染
function listToTree (list) {
let result = []
for (let item of list) {
// 如果父节点为空,则直接放入数组中
if (item.parentId == null) {
result.push(item)
} else {
// 否则找到父节点,放到父节点的 children 列表中
let index = list.findIndex(child => child.id == item.parentId)
if (list[index].children == null){
list[index].children = []
}
list[index].children.push(item)
}
}
return result
}
2. 组件编写
数据结构准备好之后,我们直接就可以开始组件编写了
首先我们需要准备一个父容器 TreeBox,用来处理整体树的逻辑、以及渲染整个第一层级的结构
<script setup>
const props = defineProps({
data: {
type: Array,
default: () => []
}
});
</script>
<template>
<div>
<TreeItem :key="item.id" v-for="item in data" :value="item"></TreeItem>
</div>
</template>
这里 props 传入的 data 就是我们上面准备好的数据结构
模板中通过 for 循环来展示我们树结构中所有顶层的节点
然后我们就可以开始定义 TreeItem 子节点了
<script setup>
const props = defineProps({
value: {
type: Object,
default: () => {}
}
});
</script>
<template>
<div>
<div class="list-box">
<div class="name">
<div >{{ value.name }}</div>
</div>
// 渲染子节点
<div class="child-box" v-if="value.children">
<tree-item v-for="item in value.children" :key="item.id" :value="item"></tree-item>
</div>
</div>
</div>
</template>
子节点也是非常简单
value 就是我们刚刚传入的顶层节点本身,然后我们在 child-box 判断节点是否有子元素列表,如果有就循环子元素列表,递归我们的 tree-item 节点
这样就可以一层一层递归渲染我们所有的子节点了
www.processon.com/view/link/6…
其实最主要是就 递归 , 也就是子节点自己来判断自己是否还需要渲染子节点
子子节点中也有判断自己是否还需要渲染子节点
这样依次判断之后,所有节点就都渲染出来了
如果我们不使用 递归子节点 而是使用 单个节点 循环来做的话,当然也可以,但是会非常的麻烦。
递归子节点 大部分情况是最省事的做法
3. 实现展开和收起
对于展开和收起我们可以用一个数组来进行标记
我们首先来看 treeBox 组件的变化
// 展开的ID数组
const expandedKeys = ref([]);
// 展开,将元素的ID放入
function expand(item) {
expandedKeys.value = [...new Set([...expandedKeys.value, item.id])];
}
// 收起,移出该元素ID
function unExpand(item) {
expandedKeys.value = expandedKeys.value.filter((child) => child != item.id);
}
// 展开所有
function expandAll() {
let list = [];
for (let item of flatTree) {
list.push(item.id);
}
expandedKeys.value = list;
}
// 收起所有
function unExpandAll() {
expandedKeys.value = [];
}
需要注意的是,上面我们使用到了 flatTree 这个是树展开后的结构。
这样可以方便的在展开所有时,插入所有需要展开的ID
const flatTree = [];
// 展开树节点到数组中
function doFlattenChildren() {
flatTree.length = 0;
if (props.data == null || props.data.length === 0) {
return;
}
function flattenChildren(node, parent) {
if (parent) {
node.parent = parent;
}
// 将节点放到 flatTree中
flatTree.push(node);
// 如果有子节点则循环放入
if (node.children) {
for (let child of node.children) {
flattenChildren(child, node);
}
}
}
for (let item of props.data) {
flattenChildren(item);
}
}
// 初始化和树结构变化时调用
doFlattenChildren();
watch(
() => props.data,
() => {
doFlattenChildren();
},
{
deep: true
}
);
最后我们需要使用到 vue 的 provide 将 treeItem 需要使用到的,展开、不展开,以及展开列表提供给子元素
provide('treeParent', {
expand,
unExpand,
expandedKeys
});
然后我们在 treeItem 中就可以很方便的实现展开收起了
const treeParent = inject('treeParent');
// 判断元素是否展开
const isExpand = computed(() => {
return treeParent.expandedKeys.value.includes(props.value.id);
});
// 展开 收起 元素
function expand(item) {
if (isExpand.value) {
treeParent.unExpand(item);
} else {
treeParent.expand(item);
}
}
模板部分
<div
:class="isExpand ? 'rotate-90' : ''"
v-if="value.children && value.children.length > 0"
style="rotate: -90deg;"
@click="expand(value)"
>▼</div>
<div>{{ value.name }}</div>
</div>
// 如果展开才展示
<div class="child-box" v-if="isExpand && value.children">
<tree-item v-for="item in value.children" :key="item.id" :value="item"></tree-item>
</div>
这样我们就轻松实现了元素的展开、收起功能
4. 节点选中
节点选中就更加简单了,我们可以定义一个 selectId 变量,然后通过改变这个变量就实现了选中功能
先看看 treeBox 组件的改变
// 新增 selectId props 和 update:selectId emit 做双向数据绑定
const props = defineProps({
data: {
type: Array,
default: () => []
},
selectId: {
type: [String, Number],
default: null
}
});
const emit = defineEmits(['update:selectId']);
// 新增 selectIdInner 变量,作为 props.selectId 的缓存
const selectIdInner = ref(props.selectId);
// 选中方法,同时更新内部和外部变量
function selectItem(item) {
selectIdInner.value = item.id;
emit('update:selectId', item.id);
}
// 如果传入的 props.selectId 变化 更新 内部缓存
watch(
() => props.selectId,
() => {
selectIdInner.value = props.selectId;
}
);
// 将 selectIdInner 缓存和 selectItem方法 传递给子组件
provide('treeParent', {
expand,
unExpand,
expandedKeys,
selectId: selectIdInner,
selectItem
});
最后子组件只需要判断是否选中就大功告成了
// 调用 treeBox 的 selectItem
function selectItem() {
treeParent.selectItem(props.value);
}
// 再 div 上 加上对应的方法,和 class 就完成了单选选中了
<div @click="selectItem" :class="{ 'active': treeParent.selectId.value == value.id }">{{ value.name }}</div>
源码可见 vuejs play
后记
这里抛砖引玉,只是实现了一个最简单的树组件
没有样式,没有选择框,只是站在一个初学者可能不太了解如何快速实现树组件的角度,做一定的分享
希望对大家有所帮助