Vue3:组件通信方式
目录
1、父子组件通信 props
1.1 父传子
1.2 子传父
2、自定义事件 custom-event
3、mitt
4、Pinia
4.1 搭建pinia环境
4.2 存储和读取数据
4.3 修改数据(三种方式)
4.4 storeToRefs
storeToRefs的使用方法
4.5 getters
4.6 $subscribe的使用
4.7 pinia的组合式写法
5、参考
1、父子组件通信 props
props可以实现父传子,也可以实现子传父
1.1 父传子
首先准备下面的父组件,里面包含数据,我设置为fatherName,然后再给子组件传递数据
<!-- 父组件 -->
<template>
<div style="height: 200px; width: 200px; background-color: aquamarine;">
<h4>父组件,名字为{{ fatherName }}</h4>
<!-- 将父组件的数据fatherName传递给子组件 -->
<child :fatherName1="fatherName"></child>
</div>
</template>
<script setup>
import child from './child.vue';
import { ref } from 'vue';
let fatherName = ref('张三')
</script>
<style scoped></style>
:fatherName1="fatherName"
-
将父组件中的
fatherName
变量的值(即'张三'
)传递给子组件的fatherName1
props。 -
子组件可以通过
fatherName1
props来接收并使用这个值
子组件代码如下:
<!-- 子组件 -->
<template>
<div style="height: 80px; width: 200px; background-color: beige;">
<h4>子组件,名字为{{ childName }}</h4>
<!-- 使用传递过来的数据 -->
<div>父亲名字为:{{ fatherName1 }}</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
//接受父组件传递的数据
defineProps(['fatherName1'])
let childName = ref('张小')
</script>
<style scoped></style>
defineProps()中一定要是数组,在父组件传入多个数据给子组件的时候,我们是将多个数据写成数组的形式,即使只传入一个数据也要写成数组的形式。
其次,上述示例中,fatherName1在父子组件中的名称必须相同,否则无法实现父子组件通信的功能
效果图如下:
1.2 子传父
我们继续上面那个示例,如果子组件需要向父组件传递它自己拥有的数据childName。那么首先我们需要在父组件那创建一个方法getChildName,并把方法传递给子组件
<!-- 父组件 -->
<template>
<div style="height: 400px; width: 200px; background-color: aquamarine;">
<h4>父组件,名字为{{ fatherName }}</h4>
<div>孩子为{{ childName }}</div>
<!-- 将父组件的数据fatherName传递给子组件,将方法传给子组件 -->
<child :fatherName1="fatherName" :sendName="getChildName"></child>
</div>
</template>
<script setup>
import child from './child.vue';
import { ref } from 'vue';
let fatherName = ref('张三')
let childName = ref('')
//接收子组件数据的方法
function getChildName(name){
childName.value = name
}
</script>
<style scoped></style>
然后,我们在子组件中接收方法,并在一个合适的地方调用即可(虽然告诉父亲名字是有点离谱,但是这个不重要哈
<!-- 子组件 -->
<template>
<div style="height: 200px; width: 200px; background-color: beige;">
<h4>子组件,名字为{{ childName }}</h4>
<!-- 使用传递过来的数据 -->
<div>父亲名字为:{{ fatherName1 }}</div>
<button @click="sendName(childName)">告诉父亲自己的名字</button>
</div>
</template>
<script setup>
import { ref } from 'vue';
//接受父组件传递的数据
defineProps(['fatherName1', 'sendName'])
let childName = ref('张小')
</script>
<style scoped></style>
效果如下:先是没有点击按钮的效果,第二张图是点击按钮后的效果
子传父稍微麻烦和被动一点
2、自定义事件 custom-event
首先明白 $event 是事件对象,一个特殊的占位符,那么我们也可以自定义一个事件对象用来父子组件间的通信
自定义事件是专门用来实现子传父的组件通信功能的
父组件的代码如下:
<!-- 父子组件通信 -->
<template>
<div style="height: 400px; width: 200px; background-color: aquamarine;">
<h4>父组件,名字为{{ fatherName }}</h4>
<div>孩子为{{ childName }}</div>
<!-- 子组件,绑定一个自定义事件abc,只要abc事件被触发,那么xyz就会被调用,abc在子组件中声明 -->
<child @abc="xyz"></child>
</div>
</template>
<script setup>
import child from './child.vue';
import { ref } from 'vue';
let fatherName = ref('张三')
let childName = ref('')
//name是在子组件中传递的数据决定的
function xyz(name){
childName.value=name
}
</script>
<style scoped></style>
子组件的代码如下:子组件的话需要使用defineEmits()来声明事件,然后在合适的地方使用emit()调用事件去触发上面父组件定义的xyz方法
<!-- 父子组件通信 -->
<template>
<div style="height: 200px; width: 200px; background-color: beige;">
<h4>子组件,名字为{{ childName }}</h4>
<!-- 点击按钮触发abc事件,逗号后边是传递的数据 -->
<button @click="emit('abc', childName)">介绍自己</button>
</div>
</template>
<script setup>
import { ref } from 'vue';
const emit = defineEmits(['abc']) //声明事件
let childName = ref('张小')
</script>
<style scoped></style>
点击按钮后呈现的效果如下:
感觉这个通信方式和第一个通信方式差不多,都是在父组件定义方法再到子组件中触发这个方法。子组件都需要声明方法并触发方法,只是略微有一些语句上的差别
在理解这个自定义事件的时候,可以将<child @abc="xyz"></child> 中的@abc="xyz"类比成@click="xyz"来理解,只是click点击事件是官方写好的事件,abc是我们自定义的事件
3、mitt
这个方法可以在任意组件中使用。
首先需要安装mitt,执行指令:npm i mitt,然后在utils文件夹下创建emitter.js文件,文件中的内容写下以下内容:
// 引入mitt
import mitt from 'mitt'
//调用mitt得到emitter,可以绑定和触发事件
const emitter = mitt()
export default emitter
然后在需要组件通信的vue组件中加入 import emitter from './utils/emitter' 这句话
我们需要明白emitter.on()是绑定事件,emitter.off()是解绑事件,emitter.emit()触发事件,emitter.all()是拿到所有绑定的事件
提供数据的组件要触发事件,接收数据的组件要绑定事件
接下来我们看示例即可:示例是两个孩子,孩子1要问孩子2的名字
孩子1代码示例:
<!-- 孩子1 -->
<template>
<div style="height: 200px; width: 400px; background-color: beige;">
<h4>孩子1,名字为{{ childName }}</h4>
<h4>孩子2的名字为:{{ friendName!=='' ? friendName : '???' }}</h4>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue';
import emitter from '../utils/emitter';
let childName = ref('张大')
let friendName = ref('')
onMounted(()=>{
//在组件挂载完毕的时候,让张大问张二的名字,value即接收到的childName
emitter.on('sentFrindName',(value)=>{
friendName.value = value
})
})
</script>
<style scoped></style>
孩子2代码如下:
<!-- 孩子2 -->
<template>
<div style="height: 200px; width: 400px; background-color: beige;">
<h4>孩子2,名字为{{ childName }}</h4>
<!-- 触发事件,把childName作为参数传递过去 -->
<button size="small" @click="emitter.emit('sentFrindName',childName)">介绍自己</button>
</div>
</template>
<script setup>
import { ref } from 'vue';
import emitter from '../utils/emitter';
let childName = ref('张二')
</script>
<style scoped></style>
点击按钮的网页效果如下:
mitt是非常轻量级的,跟自定义事件比较相似,只是不用受限制于父子组件中,在任意组件中都能实现组件通信,但是在多人开发中难以追踪事件流,不适合大型项目。
-
对于复杂项目,建议使用状态管理库(如 Vuex 或 Pinia)。
-
对于父子组件通信,优先使用
props
和emit
感觉mitt的位置很尴尬啊,但是自己做小型项目开发用这个会方便一点
4、Pinia
官网链接:简介 | Pinia (vuejs.org)
pinia是集中式状态管理,vue2项目比较常用的是vuex,但是vue3项目现在比较推荐pinia,更加符合直觉
多个组件共有的数据才用到pinia,一个组件独有的数据就不建议写到pinia上
4.1 搭建pinia环境
第一步: npm install pinia --->在控制台中输入命令安装pinia
第二步:在main.js文件中,引入创建和安装pinia
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
//引入pinia
import { createPinia } from 'pinia'
const app = createApp(App)
//创建pinia,最好在app创建完之后
const pinia = createPinia()
//安装pinia
app.use(pinia)
app.mount('#app')
4.2 存储和读取数据
在src文件创建一个名为store的文件夹,在store文件夹下新建xxx.js文件,xxx的命名应该体现出来这个js文件写的内容与什么相关,达到望文知意的效果。数据是直接存储到这个xxx.js文件中,不用在组件中定义存储
因为示例仍然是张大问张二名字,所以我将在store文件下新建name.js
//name.js文件
import { defineStore } from "pinia";
export const useNameStore = defineStore('child2',{
//真正存储数据的地方
state(){
return {
child2Name:'张二'
}
},
})
-
defineStore
: 这是 Pinia 提供的一个函数,用于定义一个状态存储(store)。每个 store 都有一个唯一的名称,这里是'child2'
。 -
state
: 这是一个函数,返回一个对象,这个对象就是 store 的状态。在这个例子中,state
返回了一个对象,其中包含一个属性child2Name
,初始值为'张二'
。
child1组件:
<!-- 孩子1 -->
<template>
<div style="height: 200px; width: 400px; background-color: beige;">
<h4>孩子1,名字为{{ childName }}</h4>
<h4>孩子2的名字为:{{ nameStore.child2Name }}</h4>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { useNameStore } from '../store/name';
let childName = ref('张大')
const nameStore = useNameStore()
</script>
<style scoped></style>
-
useNameStore
: 这是之前定义的 Pinia store。通过调用useNameStore()
,可以在组件中访问和修改 store 中的状态。 -
nameStore
: 这是 store 的一个实例,通过它可以直接访问 store 中的状态,比如nameStore.child2Name
child2组件同理获得张二自己的名字,这里就不展示了。
相当于张大想要知道张二名字,不需要通过询问张二,直接问第三方pinia即可知道,张二显示自己的名字也需要从第三方pinia中拿到
4.3 修改数据(三种方式)
比如说我们张二要改名字,改名为张三
第一种:直接在组件在获取后修改即可
<!-- 孩子2 -->
<template>
<div style="height: 200px; width: 400px; background-color: beige;">
<h4>孩子2,名字为{{ nameStore.child2Name }}</h4>
<button size="small" @click="changeName">我要改名</button>
</div>
</template>
<script setup>
import { useNameStore } from '../store/name';
const nameStore = useNameStore()
function changeName(){
//直接修改即可
nameStore.child2Name = "张三"
}
</script>
<style scoped></style>
效果如下:(点击按钮前后对比)
第二种:适合数据多的时候用
给孩子多写一个数据,school表示就读学校
两个文件写一块了,大家注意区别
//name.js文件
import { defineStore } from "pinia";
export const useNameStore = defineStore('child2',{
//真正存储数据的地方
state(){
return {
child2Name:'张二',
school:"人民小学"
}
},
})
<!--child2.vue文件-->
<!-- 孩子2 -->
<template>
<div style="height: 200px; width: 400px; background-color: beige;">
<h4>孩子2,名字为{{ nameStore.child2Name }}</h4>
<h4>就读学校{{ nameStore.school }}</h4>
<button size="small" @click="change">我要改名转学</button>
</div>
</template>
<script setup>
import { useNameStore } from '../store/name';
const nameStore = useNameStore()
//修改数据
function change(){
//批量修改
nameStore.$patch({
child2Name:'张三',
school:'幸福小学'
})
}
</script>
<style scoped></style>
第三种:最麻烦的一种
name.js文件如下:
import { defineStore } from "pinia";
export const useNameStore = defineStore('child2',{
//真正存储数据的地方
state(){
return {
child2Name:'张二',
school:"人民小学"
}
},
//方法, 放置的是一个个的方法,用于响应组件中的“动作”
actions:{
changeName(name, sch){
//修改数据,用this来拿到需要修改的数据然后赋值即可
this.child2Name=name
this.school=sch
}
}
})
child2.vue文件如下:
<!-- 孩子2 -->
<template>
<div style="height: 200px; width: 400px; background-color: beige;">
<h4>孩子2,名字为{{ nameStore.child2Name }}</h4>
<h4>就读学校{{ nameStore.school }}</h4>
<button size="small" @click="nameStore.changeName('张三','幸福小学')">我要改名转学</button>
</div>
</template>
<script setup>
import { useNameStore } from '../store/name';
const nameStore = useNameStore()
</script>
<style scoped></style>
4.4 storeToRefs
-
storeToRefs
的作用:-
将 store 中的状态转换为响应式引用(ref),保持响应性
-
避免直接解构 store 导致的状态丢失问题
-
-
storeToRefs
的适用范围:-
仅适用于 store 中的 状态(state),不适用于 getters 或 actions
-
如果你需要解构 getters 或 actions,可以直接从 store 实例中解构
-
-
与直接使用 store 的区别:
-
直接使用 store 实例(如
nameStore.child2Name
)也是响应式的 -
使用
storeToRefs
主要是为了方便在模板中直接使用状态,而不需要每次都写nameStore.xxx
-
storeToRefs的使用方法
1、导入 storeToRefs
import { storeToRefs } from 'pinia';
2、在组件中使用
import { useNameStore } from '../store/name';
import { storeToRefs } from 'pinia';
const nameStore = useNameStore();
const { child2Name, school } = storeToRefs(nameStore);
完整代码:
<template>
<div style="height: 200px; width: 400px; background-color: beige;">
<h4>孩子2,名字为{{ child2Name }}</h4>
<h4>就读学校{{ school }}</h4>
<button size="small" @click="nameStore.changeName('张三', '幸福小学')">
我要改名转学
</button>
</div>
</template>
<script setup>
import { useNameStore } from '../store/name';
import { storeToRefs } from 'pinia';
const nameStore = useNameStore();
const { child2Name, school } = storeToRefs(nameStore);
</script>
<style scoped></style>
4.5 getters
getter是 Pinia 中用于从状态(state)中派生出计算属性的函数。类似于 Vuex 中的 getter,它允许你基于 store 的状态进行复杂计算,并返回结果
定义 Getter:在 store 中通过 getters
属性定义 getter 函数。这些函数接收 state
作为第一个参数,可以访问 store 的状态
import { defineStore } from 'pinia';
export const useStore = defineStore('main', {
state: () => ({
count: 0,
}),
getters: {
doubleCount: (state) => state.count * 2,
},
});
访问 Getter:在组件或其他地方通过 store 实例访问 getter。Getter 会被缓存,只有在依赖的状态发生变化时才会重新计算
import { useStore } from '@/stores/main';
const store = useStore();
console.log(store.doubleCount); // 输出 doubleCount 的值
Getter 传参:Getter 可以通过返回一个函数来接受参数
getters: {
multiplyCount: (state) => (factor) => state.count * factor,
}
使用时:
console.log(store.multiplyCount(3)); // 输出 count * 3 的结果
4.6 $subscribe的使用
$subscribe
是 Pinia 提供的一个方法,用于监听 store 中状态(state)的变化。它类似于 Vue 的 watch
,但专门用于监听整个 store 的状态变更
使用方式:
1、在组件或任何地方,通过 store 实例调用 $subscribe
方法,传入一个回调函数。该回调函数会在 store 的状态发生变化时触发
import { useStore } from '@/stores/main';
const store = useStore();
store.$subscribe((mutation, state) => {
console.log('状态变化:', mutation);
console.log('当前状态:', state);
});
-
mutation
:包含状态变化的相关信息,例如:-
type
:变更类型(如direct
直接修改,或patch object
/patch function
使用$patch
修改)。 -
storeId
:触发变更的 store 的 ID。 -
payload
:传递的数据(如使用$patch
时的参数)。
-
-
state
:当前 store 的最新状态。
2、监听特定状态:如果需要监听特定的状态变化,可以在回调函数中通过state或者mutation进行判断
store.$subscribe((mutation, state) => {
if (state.count > 10) {
console.log('count 大于 10');
}
});
3、取消监听:$subscribe
返回一个函数,调用该函数可以取消监听
const unsubscribe = store.$subscribe((mutation, state) => {
console.log('状态变化:', state);
});
// 取消监听
unsubscribe();
4、配置选项
$subscribe
的第二个参数是一个配置对象,支持以下选项:
-
detached: boolean
:默认为false
。如果为true
,即使组件卸载,监听器也不会被自动移除 -
deep: boolean
:默认为false
。如果为true
,会深度监听状态的变化
store.$subscribe(
(mutation, state) => {
console.log('状态变化:', state);
},
{ detached: true, deep: true }
);
4.7 pinia的组合式写法
相当于直接把 defineStore('xx', ()=>{}) 中的()=>{}当成setup的东西在写,只是要记得return东西出去,其他在组件中的使用方式一样没有变化
import { defineStore } from "pinia";
// 选项式写法
// export const useNameStore = defineStore('child2',{
// //真正存储数据的地方
// state(){
// return {
// child2Name:'张二',
// school:"人民小学"
// }
// },
// //方法, 放置的是一个个的方法,用于响应组件中的“动作”
// actions:{
// changeName(name, sch){
// //修改数据,用this来拿到需要修改的数据然后赋值即可
// this.child2Name=name
// this.school=sch
// }
// }
// })
//组合式写法
import { ref } from "vue";
export const useNameStore = defineStore('child2',()=>{
let child2Name = ref('张二')
let school = ref('人民小学')
function changeName(name, sch){
//修改数据,用this来拿到需要修改的数据然后赋值即可
this.child2Name=name
this.school=sch
}
//不要忘记return出去
return {child2Name, school, changeName}
})
5、参考
052.组件通信_方式1_props_哔哩哔哩_bilibili