This article takes a Vue recursive component as an example to explore the problem that events cannot be triggered after multiple layers of nesting. You can check the Demo to quickly understand this example.
Suppose we already know that Vue components commonly have parent-child component communication and sibling component communication. The communication between parent and child components is very simple. The parent component will pass data down to the child component through props. When the child component has something to tell the parent component, it will tell the parent component through the $emit event. So when there is no parent-child relationship between the two components, how to pass data?
Let's take a look at this example first:
Recursive nested component parameter passing
We encapsulate a sub-component named NestedDir (meaning nested directory), the content is as follows (the element ui component is used):
<!-- NestedDir.vue -->
<template>
<ul class="nest_wrapper">
<li v-for="(el, index) in nested" :key="index">
<div v-if="el.type ==='dir'" class="dir">
<p>{{el.name}}</p>
<div class="btn_group">
<el-button type="warning" size="mini" @click="add({id: el.id, type: 'dir'})">新增目录</el-button>
<el-button type="warning" size="mini" @click="add({id: el.id, type: 'file'})">新增文件</el-button>
</div>
</div>
<div v-if="el.type ==='file'" class="file">
<p>{{el.name}}</p>
</div>
<NestedDir v-if="el.children" :nested="el.children"/>
</li>
</ul>
</template>
<script>
export default {
name: "NestedDir",
props: {
nested: {
type: Array,
}
},
methods: {
add(el) {
this.$emit('change', el)
}
}
}
</script>
It can be seen that this NestedDir receives the data of the nested array type from the parent, and its internal click to add a new directory and a new file can trigger the change event monitored by the parent. What is more special is that this component calls itself:
<NestedDir v-if="el.children" :nested="el.children"/>
However, it should be noted that when calling ourselves, we did not listen to the change event from it on it, which is also the reason why clicking the new button in the secondary directory is invalid.
The nested data structure we pass to it looks something like this:
[{
"id": 1,
"name": "目录1",
"type": "dir",
"children": [{
"id": 2,
"name": "目录3",
"type": "dir",
"children": [],
"pid": 1
}, {
"id": 3,
"name": "文件2",
"type": "file",
"pid": 1
}]
}, {
"id": 4,
"name": "目录2",
"type": "dir",
"children": []
}, {
"id": 5,
"name": "文件1",
"type": "file",
"children": []
}]
Call NestedDir in the parent component:
<!-- directory.vue -->
<template>
<div style="width: 50%;box-shadow: 0 0 4px 2px rgba(0,0,0,.1);margin: 10px auto;padding-bottom: 10px;">
<!-- 顶部按钮组 -->
<div class="btn_group">
<el-button type="warning" size="mini" @click="showDialog({type: 'dir'})">新增目录</el-button>
<el-button type="warning" size="mini" @click="showDialog({type: 'file'})">新增文件</el-button>
</div>
<!-- 嵌套组件 -->
<NestedDir :nested="catalog" @change="handleChange"/>
<!-- 新增弹出框 -->
<el-dialog :title="title" :visible.sync="dialogFormVisible" width="300px">
<el-form :model="form">
<el-form-item label="名称">
<el-input v-model="form.name" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false">取 消</el-button>
<el-button type="primary" @click="confirm">确 定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import NestedDir from "./NestedDir";
export default {
name: "directory",
components: {
NestedDir
},
created() {
this.catalog = this.getTree()
},
computed: {
maxId() {
return this.arr.lastIndex + 2
},
topNodes() {
this.arr.forEach(item => {
if (item.children) item.children = []
})
return this.arr.filter(item => !item.pid)
}
},
data() {
return {
arr: [
{id: 1, name: '目录1', type: 'dir', children: []},
{id: 2, name: '目录3', type: 'dir', children: [], pid: 1},
{id: 3, name: '文件2', type: 'file', pid: 1},
{id: 4, name: '目录2', type: 'dir', children: []},
{id: 5, name: '文件1', type: 'file'},
],
title: '',
dialogFormVisible: false,
form: {
id: '',
name: '',
type: '',
pid: ''
},
catalog: []
}
},
methods: {
handleChange(el) {
this.showDialog(el)
},
confirm() {
this.arr.push({...this.form})
this.dialogFormVisible = false
this.catalog = this.getTree()
this.form = {
id: '',
name: '',
type: '',
pid: '' , // 父级的id
}
},
showDialog(el) {
if (el.type === 'dir') {
this.title = '新增目录'
this.form.children = []
this.form.type = 'dir'
} else {
this.title = '新增文件'
this.form.type = 'file'
}
if (el.id) {
this.form.pid = el.id
this.form.id = this.maxId
} else {
this.form.id = this.maxId
}
this.dialogFormVisible = true
},
getTree() {
this.topNodes.forEach(node => {
this.getChildren(this.arr, node.children, node.id)
})
return this.topNodes
},
getChildren(data, result, pid) {
for (let item of data) {
if (item.pid === pid) {
const newItem = {...item, children: []}
result.push(newItem)
this.getChildren(data, newItem.children, item.id)
}
}
}
}
}
</script>
<style scoped>
.btn_group {
padding: 20px 10px;
background-color: rgba(87, 129, 189, 0.13);
}
</style>
The rendered page looks like this:
Deep recursive component events are lost
We constructed a directory structure that theoretically can be nested infinitely, but after testing, it was found that the click of the new button on the secondary directory did not respond, because we called it in NestedDir and did not monitor the internal The change event (mentioned above), so it can't trigger the parent-parent's listening event.
How to solve?
- Also listen to the change event when calling recursively, and pass it to the outermost component indirectly (this is the easiest way to think of it, but if the components are deeply nested, it's a nightmare)
- EventBus (event bus)
EventBus
What about EventBus?
It's actually a Vue instance, with $emit, $on, and $off methods, allowing data to be passed from one component to another without resorting to parent components. The specific method is to $emit in one component and $on in another component, which can be done as follows:
// main.js
import Vue from 'vue'
import App from './App.vue'
export const eventBus = new Vue(); // creating an event bus.
new Vue({
render: h => h(App),
}).$mount('#app')
So let's transform directory.vue, only need to change the srcipt part:
<script>
import NestedDir from "./NestedDir";
import { eventBus } from "../main";
export default {
name: "directory",
components: {
NestedDir
},
created() {
this.catalog = this.getTree()
eventBus.$on('change', function (data) {
// todo 向之前一样处理即可
})
},
destroyed() {
eventBus.$off('change')
},
computed: {
maxId() {
return this.arr.lastIndex + 2
}
},
data() {
return {
arr: [
{id: 1, name: '目录1', type: 'dir', children: []},
{id: 2, name: '目录3', type: 'dir', children: [], pid: 1},
{id: 3, name: '文件2', type: 'file', pid: 1},
{id: 4, name: '目录2', type: 'dir', children: []},
{id: 5, name: '文件1', type: 'file'},
],
title: '',
dialogFormVisible: false,
form: {
id: '',
name: '',
type: '',
pid: ''
},
catalog: []
}
},
methods: {
handleChange(el) {
this.showDialog(el)
},
confirm() {
this.arr.push({...this.form})
this.dialogFormVisible = false
this.catalog = this.getTree()
this.form = {
id: '',
name: '',
type: '',
pid: '' , // 父级的id
}
},
showDialog(el) {
if (el.type === 'dir') {
this.title = '新增目录'
this.form.children = []
this.form.type = 'dir'
} else {
this.title = '新增文件'
this.form.type = 'file'
}
if (el.id) {
this.form.pid = el.id
this.form.id = this.maxId
} else {
this.form.id = this.maxId
}
this.dialogFormVisible = true
},
getTree() {
this.topNodes.forEach(node => {
this.getChildren(this.arr, node.children, node.id)
})
return this.topNodes
},
getChildren(data, result, pid) {
for (let item of data) {
if (item.pid === pid) {
const newItem = {...item, children: []}
result.push(newItem)
this.getChildren(data, newItem.children, item.id)
}
}
}
}
}
</script>
Introduced import { eventBus } from "../main";
Add event listeners when the page is created, and remove event listeners when it is destroyed:
created() {
eventBus.$on('change', function (data) {
this.handleChange(data)
})
},
destroyed() {
eventBus.$off('change')
}
Corresponding changes also need to be made in NestedDir.vue, just modify the add method in methods:
import { eventBus } from "../main";
//...略
methods: {
add(el) {
// this.$emit('change', el)
eventBus.$emit('change', el)
}
}
In this way, click the new button of the secondary directory, and the pop-up box can be triggered normally.
The above eventBus is only valid in Vue2, the $on, $off methods have been removed in Vue3, so the next article intends to make a Vue plugin to deal with this situation similar to Pub/Sub.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。