工作忙完之后,现在就有时间去总结最近遇到的难题跟使用的扫技巧。接下来的技巧就是vue的递归组件。那么什么是递归组件呢?其实很简单,就是组件自己调用自己,现在我们从一个需求开始使用递归组件。(没错,又是Vue2实现的,真tm苦逼啊~)
需求
接下来我们需要实现一个树形结构的评论列表展示。具体效果如下:
实现
组件的内容比较简单,主要是为了展示如何使用递归组件。这是ChatItem的组件:
js
<template>
<div class="tree-item">
<div class="container">
<div class="content">{{ treeData.content }}</div>
<div class="reply-btn">回复</div>
</div>
<Editor v-if="treeData.show" />
</div>
</template>
<script>
import Editor from "./Editor.vue";
export default {
components: {
Editor,
},
props: {
index: {
type: Number,
default: 0,
},
treeData: {
type: Object,
default() {
return {};
},
},
},
methods: {
},
};
</script>
<style scoped>
.container {
display: flex;
}
.reply-btn {
color: hotpink;
cursor: pointer;
margin-left: 10px;
}
</style>
这是父组件的代码:
javascript
<template>
<div id="app">
<template v-for="(tree, index) in treeData">
<chat-item
:treeData="tree"
:index="index"
:key="tree.id"
/>
</template>
</div>
</template>
<script>
import chatItem from "./components/chatItem.vue";
export default {
components: {
chatItem,
},
data() {
return {
treeData: [
{
id: 1,
content: "这是内容1",
show: false,
children: [
{
id: 11,
content: "这是内容11",
show: false,
children: [
{
id: 111,
content: "这是内容111",
show: false,
children: [],
},
],
},
],
},
{
id: 2,
content: "这是内容2",
show: false,
children: [
{
id: 21,
content: "这是内容21",
show: false,
children: [],
},
],
},
{
id: 3,
content: "这是内容3",
show: false,
children: [
{
id: 32,
content: "这是内容32",
show: false,
children: [],
},
{
id: 33,
content: "这是内容33",
show: false,
children: [
{
id: 332,
content: "这是内容332",
show: false,
children: [],
},
],
},
],
},
{
id: 4,
content: "这是内容4",
show: false,
children: [],
},
],
trigger: [],
};
},
methods: {},
};
</script>
<style></style>
那么如何实现组件自己调用自己呢?其实很简单,看来面代码:
diff
<template>
<div class="tree-item">
<div class="container">
<div class="content">{{ treeData.content }}</div>
<div class="reply-btn">回复</div>
</div>
<Editor v-if="treeData.show" />
+ <div
+ class="sub-container"
+ v-if="treeData.children && treeData.children.length > 0"
+ >
+ <template v-for="(sub, index) in treeData.children">
+ <chat-item
+ :index="index"
+ :key="sub.id"
+ :tree-data="sub"
+ >
+ </chat-item>
+ </template>
+ </div>
</div>
</template>
<script>
import Editor from "./Editor.vue";
export default {
+ name: "ChatItem",
components: {
Editor,
},
props: {
index: {
type: Number,
default: 0,
},
treeData: {
type: Object,
default() {
return {};
},
},
},
data() {
return {
level: 0,
};
},
methods: {},
};
</script>
<style scoped>
.container {
display: flex;
}
.reply-btn {
color: hotpink;
cursor: pointer;
margin-left: 10px;
}
+ .sub-container {
+ margin-left: 20px;
+ }
</style>
这样就完成了组件递归的使用,但是又会遇到一个问题,这种数据格式下,我怎么才能精确的获取到数据呢?对我这个经常用ElementUI库组件的人来说,树形组件的index一般都是一个数组,然后就是按照一级一级的往下,放具体的位置index,实现这个东西也不能,可以看看下面:
js
<template>
<div class="tree-item">
<div class="container">
<div class="content">{{ treeData.content }}</div>
<div class="reply-btn" @click="showReply">回复</div>
</div>
<Editor v-if="treeData.show" />
<div
class="sub-container"
v-if="treeData.children && treeData.children.length > 0"
>
<template v-for="(sub, index) in treeData.children">
<chat-item
:index="index"
:key="sub.id"
:tree-data="sub"
@node-click="handleNodeClick"
>
</chat-item>
</template>
</div>
</div>
</template>
<script>
import Editor from "./Editor.vue";
export default {
name: "ChatItem",
components: {
Editor,
},
props: {
index: {
type: Number,
default: 0,
},
treeData: {
type: Object,
default() {
return {};
},
},
},
data() {
return {
level: 0,
};
},
methods: {
showReply() {
this.$emit("node-click", {
node: this.treeData,
index: [this.index],
show: !this.treeData.show,
});
},
handleNodeClick({ node, index, show }) {
this.$emit("node-click", {
node,
index: [this.index, ...index],
show,
});
},
},
};
</script>
<style scoped>
.container {
display: flex;
}
.reply-btn {
color: hotpink;
cursor: pointer;
margin-left: 10px;
}
.sub-container {
margin-left: 20px;
}
</style>
说起来会比较绕,实际上就是,点击回复的时候,我们emit一个方法,把当前的数据,index,以及show导出。那么对于chatItem的上级来说,我们就需要用方法去接受它传出来的值,拿到值的时候,其实只需要将当前上级的index,放到index数组的第一位即可,紧接着就继续用emit出这个方法。这样就可以实现层层emit,最终获取到数据的index具体数组。那么外层既可以拿到这个具体是数据,看看下面的gif:
获取到具体的位置,又能拿到当前点击的数据,接下来要实现的就是这个回复框了。需求比较简单,就是我只希望出现一个回复框,点击的时候其他的回复框需要隐藏。
我第一个想到的就是排插 ,什么意思呢?就是我先将所有的节点,遍历一遍,全部设置为false不显示,然后再将目标数据设置为true 。这个方法最大的问题就是这个遍历,层级较深时这遍历起来的时间复杂度就比较恐怖了。
那么另外一个方法就是:我直接记住你每次点击的数据,然后在下一个点击的时候,将你这个数据的值设置为false,然后将当前数据的值设置为ture,这样就不需要去遍历数据了 。这就是为什么,我需要获取当前数据的index位置,这样就可以方便我去操作具体的数据。看下面父组件代码:
js
<template>
<div id="app">
<template v-for="(tree, index) in treeData">
<chat-item
:treeData="tree"
:index="index"
:key="tree.id"
@node-click="handleNodeClick"
/>
</template>
</div>
</template>
<script>
import chatItem from "./components/chatItem.vue";
export default {
name: "App",
components: {
chatItem,
},
data() {
return {
treeData: [
{
id: 1,
content: "这是内容1",
show: false,
children: [
{
id: 11,
content: "这是内容11",
show: false,
children: [
{
id: 111,
content: "这是内容111",
show: false,
children: [],
},
],
},
],
},
{
id: 2,
content: "这是内容2",
show: false,
children: [
{
id: 21,
content: "这是内容21",
show: false,
children: [],
},
],
},
{
id: 3,
content: "这是内容3",
show: false,
children: [
{
id: 32,
content: "这是内容32",
show: false,
children: [],
},
{
id: 33,
content: "这是内容33",
show: false,
children: [
{
id: 332,
content: "这是内容332",
show: false,
children: [],
},
],
},
],
},
{
id: 4,
content: "这是内容4",
show: false,
children: [],
},
],
trigger: [],
};
},
methods: {
handleNodeClick({ node, index, show }) {
if (show) {
if (this.trigger.length) {
// 没有数据的时候,说明是第一次点击
const targetIndex = this.trigger.shift();
const target = this.getTargetNode(targetIndex);
target.show = false;
}
node.show = true;
this.trigger.push(index);
} else {
// false时候说明这个时候点击的是同一个
node.show = false;
this.trigger.shift();
}
},
getTargetNode(index) {
// 除了第一层,其他都是children下面的数据
let parent = this.treeData[index[0]];
let i = 1;
if (index.length === 1) {
return parent;
} else {
while (i < index.length) {
parent = parent.children[index[i]];
i++;
}
return parent;
}
},
},
};
</script>
<style></style>
这样方法挺不错,但是你可能会说,你为什么不直接将node节点放到数据呢?这样你连这个index数据都不需要了,而且也有id可以作为两次点击是否一样的判断。确实,这样会更简单,但是嘛写这个不就是为了锻炼一下自己的能力的,都搞那么简单就没必要了是不是。下面是完整的ChatItem的代码:
js
<template>
<div class="tree-item">
<div class="container">
<div class="content">{{ treeData.content }}</div>
<div class="reply-btn" @click="showReply">回复</div>
</div>
<Editor v-if="treeData.show" />
<div
class="sub-container"
v-if="treeData.children && treeData.children.length > 0"
>
<template v-for="(sub, index) in treeData.children">
<chat-item
:index="index"
:key="sub.id"
:tree-data="sub"
@node-click="handleNodeClick"
>
</chat-item>
</template>
</div>
</div>
</template>
<script>
import Editor from "./Editor.vue";
export default {
name: "ChatItem",
components: {
Editor,
},
props: {
index: {
type: Number,
default: 0,
},
treeData: {
type: Object,
default() {
return {};
},
},
},
data() {
return {
level: 0,
};
},
methods: {
showReply() {
this.$emit("node-click", {
node: this.treeData,
index: [this.index],
show: !this.treeData.show,
});
},
handleNodeClick({ node, index, show }) {
this.$emit("node-click", {
node,
index: [this.index, ...index],
show,
});
},
},
};
</script>
<style scoped>
.container {
display: flex;
}
.reply-btn {
color: hotpink;
cursor: pointer;
margin-left: 10px;
}
.sub-container {
margin-left: 20px;
}
</style>
主要是怕上面的代码可能会删除多了,怕大家运行起来不行。
最后的吹牛
终于是又更新了文章,如果大家觉得受用的话,可以给我点个赞收藏啥的,已经是3级很久了,希望大伙给我升升,拜托了。文章有什么疑问或者问题欢迎大家在评论区留言,谢谢了(砰砰砰)。