在写项目的时候,经常要使用父子组件通讯,我已经写了很多篇博客来介绍父子组件通讯了,vue中的父子组件通讯方式有差不多10来种,最常用的就那么一两种,这里我介绍其中我认为最基础的两种。因为目标是在父组件中,通过点击按钮事件来打开子组件中的el-dialog对话框,所以比传统的父传子要复杂一点。
先捋一下逻辑:
先看看父组件中的写法
<template>
<BreadCrumb ref="breadCrumb" :item="item"></BreadCrumb>
<div class="table-wrapped">
<div class="table-top">
<div class="table-header">
<div class="search-wrapped">
<el-input v-model="input1" class="w-50 m-2" placeholder="输入账号搜索" :prefix-icon="Search" />
</div>
<div class="button-wrapped">
<el-button type="primary" @click="create">添加产品管理员</el-button>
</div>
</div>
</div>
</div>
<CreateAdmin :isShow="isShowDialog"></CreateAdmin>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { Search } from '@element-plus/icons-vue'
import BreadCrumb from '@/components/BreadCrumb.vue';
import CreateAdmin from '../components/CreateAdmin.vue'
const isShowDialog = ref(false)
const item = ref({
first: '用户管理',
second: '产品管理员'
})
const input1 = ref('')
const tableData = ref([])
const create = () => {
isShowDialog.value = true
}
</script>
<style lang="scss" scoped>
</style>
父组件中的逻辑是,点击按钮后,将isShowDialog的值改为true,同时把isShowDialog赋值给isShow传递给子组件
再看看子组件中的写法:
<template>
<el-dialog v-model="dialogVisible" title="创建管理员" width="30%" center>
</el-dialog>
</template>
<script setup>
import { onMounted, ref, watch } from 'vue'
const dialogVisible = ref(false)
const props = defineProps({
isShow: {
type: Boolean,
default: false
}
})
watch(() => props.isShow, (val) => {
console.log(val)
dialogVisible.value = val
}, { immediate: true })
</script>
<style scoped></style>
子组件定义props接收父组件传递过来的值,然后赋值给dialogVisible,el-dialog根据这个dialogVisible来控制对话框是否展示
而且还使用watch深度监视了isShow的变化,当值变化时,马上把最新的值传递给dialogVisible
理论上,这样是不是可以很好的控制对话框的打开与关闭呢?实际上,根本就没法控制,也不能这么说,就第一次可以控制,然后关闭对话框后,就没法正常打开对话框了,什么原因呢,因为在关闭对话框的时候,没用通知父组件来修改对应的值,所以只能正常执行一次。来梳理一下上面代码的逻辑:
因此,上面的逻辑中缺少一项,就是在关闭对话框的时候,子组件给父组件传递信号,将isShow的值改为false
因此正确的写法应该是这样的:
父组件:
<template>
<BreadCrumb ref="breadCrumb" :item="item"></BreadCrumb>
<div class="table-wrapped">
<div class="table-top">
<div class="table-header">
<div class="search-wrapped">
<el-input v-model="input1" class="w-50 m-2" placeholder="输入账号搜索" :prefix-icon="Search" />
</div>
<div class="button-wrapped">
<el-button type="primary" @click="create">添加产品管理员</el-button>
</div>
</div>
</div>
<CreateAdmin :isShow="isShowDialog" @close="closeDialog"></CreateAdmin>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { Search } from '@element-plus/icons-vue'
import BreadCrumb from '@/components/BreadCrumb.vue';
import CreateAdmin from '../components/CreateAdmin.vue'
const isShowDialog = ref(false)
const item = ref({
first: '用户管理',
second: '产品管理员'
})
const input1 = ref('')
const tableData = ref([])
const create = () => {
isShowDialog.value = true
}
const closeDialog = (val) => {
isShowDialog.value = val
}
</script>
<style lang="scss" scoped>
</style>
子组件:
<template>
<el-dialog v-model="dialogVisible" title="创建管理员" width="30%" center @close="closeDialog">
</el-dialog>
</template>
<script setup>
import { onMounted, ref, watch } from 'vue'
import { creatAdminAPI } from "@/apis/userinfo";
const dialogVisible = ref(false)
const props = defineProps({
isShow: {
type: Boolean,
default: false
}
})
const emits = defineEmits(['close'])
const closeDialog = () => {
emits('close', false)
}
watch(() => props.isShow, (val) => {
console.log(val)
dialogVisible.value = val
}, { immediate: true })
</script>
<style scoped></style>
先看子组件,在子组件的el-dialog对话框中,除了v-model="dialogVisible"
这个关键属性外,还定义了它的关闭事件,当关闭对话框的时候,子组件向父组件发送一个close指令,并传递一个false值给父组件
再看父组件,父组件接收close事件,并在closeDialog中将isShowDialog的值设置为子组件传递过来的值,也就是false
这样,就可以来回控制了
也就是说,控制对话框的打开关闭,实际上要实现父传子和子传父两个过程,复杂就复杂在这里
这也是我不推荐用这种方式的原因
我现在凡是涉及到父子组件传值,第一个想到的就是mitt,在vue2中,它是内置的api,叫事件总线,不知道为啥vue3移除了。mitt可以实现任意组件中的通讯,父子、兄弟、祖孙等等。
我之前写过一篇博客,专门用来解释vue3中使用第三方插件mitt实现任意组件通讯
这篇博客可能应用场景太复杂了,用在今天这个例子我觉得正合适。
使用步骤如下:
<template>
<BreadCrumb ref="breadCrumb" :item="item"></BreadCrumb>
<div class="table-wrapped">
<div class="table-top">
<div class="table-header">
<div class="search-wrapped">
<el-input v-model="input1" class="w-50 m-2" placeholder="输入账号搜索" :prefix-icon="Search" />
</div>
<div class="button-wrapped">
<el-button type="primary" @click="create">添加产品管理员</el-button>
</div>
</div>
</div>
<CreateAdmin></CreateAdmin>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { Search } from '@element-plus/icons-vue'
import BreadCrumb from '@/components/BreadCrumb.vue';
import CreateAdmin from '../components/CreateAdmin.vue'
import mitt from '@/utils/mitt'
const emitter = mitt
const item = ref({
first: '用户管理',
second: '产品管理员'
})
const input1 = ref('')
const tableData = ref([])
const create = () => {
emitter.emit('openDialog')
}
</script>
<style lang="scss" scoped>
</style>
<template>
<el-dialog v-model="dialogVisible" title="创建管理员" width="30%" center>
</el-dialog>
</template>
<script setup>
import { onMounted, ref, watch } from 'vue'
import { creatAdminAPI } from "@/apis/userinfo";
import mitt from '@/utils/mitt'
const emitter = mitt
const dialogVisible = ref(false)
emitter.on('openDialog', () => {
dialogVisible.value = true
})
</script>
<style scoped></style>
两者一对比,就发现使用mitt真的会清爽多了,这也是我为啥钟情mitt的原因
如果要存储数据,可以使用mitt结合pinia或者vuex
还有一种更简单的控制方式,不需要mitt,使用ref来实现
父组件:
<template>
<BreadCrumb ref="breadCrumb" :item="item"></BreadCrumb>
<div class="table-wrapped">
<div class="table-top">
<div class="table-header">
<div class="search-wrapped">
<el-input v-model="input1" class="w-50 m-2" placeholder="输入账号搜索" :prefix-icon="Search" />
</div>
<div class="button-wrapped">
<el-button type="primary" @click="create">添加产品管理员</el-button>
</div>
</div>
</div>
<CreateAdmin ref=createRef></CreateAdmin>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { Search } from '@element-plus/icons-vue'
import BreadCrumb from '@/components/BreadCrumb.vue';
import CreateAdmin from '../components/CreateAdmin.vue'
const item = ref({
first: '用户管理',
second: '产品管理员'
})
const input1 = ref('')
const tableData = ref([])
const createRef = ref()
const create = () => {
createRef.value.open()
}
</script>
<style lang="scss" scoped>
</style>
子组件:
<template>
<el-dialog v-model="dialogVisible" title="创建管理员" width="30%" center>
</el-dialog>
</template>
<script setup>
import { onMounted, ref, watch } from 'vue'
import { creatAdminAPI } from "@/apis/userinfo";
const dialogVisible = ref(false)
const open = () => {
dialogVisible.value = true
}
defineExpose({
open
})
</script>
<style scoped></style>
可以看到,在父组件中,使用ref获取了子组件的dom,点击按钮时,调用子组件中的open方法
在子组件中,定义一个open方法,实际上就是将dialogVisible的值改为true,随后将这个open方法暴露出去