组件之间的传值及通信
一、第一种场景 两层组件之间传值和监听值改变
// 父组件
const name = ref('1')
setTimeout(() => {
name.value = '2'
}, 1000);
// 子组件
<template>
<p>接收和监听name {{childName}}</p>
</template>
const childName = ref()
watch(()=>props.name,
(nv, pv)=>{
childName.value = nv
},{immediate: true}) // 初始值也要传过来
二、第二种场景 多层组件间传值和监听值改变
父组件
<template>
<Child />
</template>
<script lang="ts" setup>
import { ref, provide } from 'vue';
import Child from './comps/child.vue'; // 引入Child
// 层级较多时,或者想广播给所有子孙组件
provide('myProvide', {
msg: '广播的内容'
})
</script>
子组件
<template>
<p @click="clickMe">我是子组件</p>
</template>
<script lang="ts" setup>
import { inject } from 'vue';
// 接收广播的内容
const provideState = inject('myProvide')
console.log(provideState)
</script>
三、组件间事件传递和互相调用
父组件
<template>
<Child ref="childRef" :name="name" @clicked="doNext" />
</template>
<script lang="ts" setup>
import { ref, provide, onMounted } from 'vue';
import Child from './comps/child.vue'; // 引入Child
const name = ref('Famous')
function doNext() {
console.log('监听到子组件传过来信号,执行事件');
}
// 绑上子组件 childRef
const childRef = ref();
onMounted(() => {
childRef.value.closeMask(); // 需要DOM渲染完, 调用子组件方法或拿数据
});
</script>
子组件
<template>
<p @click="clickMe">我是子组件:::{{ props.name }}</p>
</template>
<script lang="ts" setup>
import { getCurrentInstance } from 'vue';
// 声明props
const props = defineProps({
name: {
type: String,
default: ''
}
});
const {proxy} = getCurrentInstance()
proxy.$parent.doNext() // 直接调用父组件的方法
// 定义emits
const $emit = defineEmits(['clicked']); // 数组形式
function clickMe() {
// 发出信号给父组件,调用父组件的方法
$emit('clicked', { test: 1 }); // 传参
}
function closeMask() {
console.log('关闭弹窗');
}
defineExpose({
closeMask // 对外部暴露方法或数据,让其父组件可通过$parent 以及 子组件通过ref 可以拿到,调用该方法
});
</script>
四、兄弟组件之间呢,vue2我们用EventBus,vue3中我们推荐mitt
(1) 安装
yarn add mitt // 或
npm install --save mitt
(2) 第一种 vue3.x的全局引入,挂载在config.globalProperties
// 在main.js 中
import mitt from 'mitt'
app.config.globalProperties.$Mitt = new mitt()
// 在组件中使用
import { getCurrentInstance } from 'vue'
const { proxy } = getCurrentInstance()
// 触发
proxy.$Mitt.emit('home_newTask', data)
// 在兄弟组件中监听
proxy.$Mitt.on('home_newTask', data => { // 事件名最好统一加前缀确保唯一
doNext()
})
(3) 第二种 封装一个ES模块,使用组件都引这一个
// 创建 Mitt.js
@/utils/Mitt.js
import mitt from 'mitt'
export default new mitt()
// 在组件中使用
import Mitt from '@/utils/Mitt.js'
// 触发
Mitt.emit('home_newTask', data)
// 在兄弟组件中监听
Mitt.on('home_newTask', data => { // 事件名最好统一加前缀确保唯一
doNext()
})
五、还有一种全局的状态,项目中很多页面共用和随时全局更新,vue2我们用Vuex,vue3中我们推荐pinia
ps 存储状态经常会遇到刷新时状态丢失的问题,需要并持久化存储 vuex 中用vuex-persistdstate,pinia 中我们用pinia-persistedstate-plugin
(1) 安装pinia 和 pinia-plugin-persist
yarn add pinia pinia-plugin-persist // 或
npm i --save pinia pinia-plugin-persist
(2) 引入
main.js 中
import App from './App.vue';
import store from '@/store';
const vm = createApp(App);
vm.use(store)
@/store/index.ts
import { createPinia } from 'pinia';
import piniaPluginPersist from 'pinia-plugin-persist';
import type { App } from 'vue';
const pinia = createPinia();
pinia.use(piniaPluginPersist); // 使用持久化插件
export default function install(app: App<Element>) {
app.use(pinia);
} // 配合main.js中app.use
export { pinia };
@/modules/user.ts
import {pinia} from '@/store';
import { defineStore } from 'pinia';
// import { loginApi } from '@/api/user/index.ts';
interface User {
nickName?: string;
cityName?: string;
}
export const useUserStore = defineStore({
id: 'user-store',
state: (): User => ({
nickName: '',
cityName: ''
}),
// getters 和actions 可以通过this访问当前实例
getters: {
// 类似计算属性,还可以调用其它store 的变量,比如 + globalStore.loginStatus,在上面引入和创建 globalStore
getWelcomeWords(): string {
return '非常欢迎' + this.nickName + '的到来!!' || '';
// 还可以return function(params){ } 就可以传入参数了
}
},
actions: {
// 类似 methods 还支持异步操作
// async setUserInfo() {
// const res = await loginApi({XXX});
// if (res.code == 200) {
// this.nickName = '尤雨溪';
// }
// }
setNickName(){
this.nickName = '尤雨溪';
}
},
// 开启数据缓存
persist: {
enabled: true,
strategies: [
{
storage: localStorage, // 默认是sessionStorage
paths: ['nickName'] // 不写key: '' 或者path 数组的话 默认是全部缓存,,
}
]
}
});
// Need to be used outside the setup
export function useUserStoreHook() {
return useUserStore(pinia);
}
// in setup
export default useUserStore(pinia);
ps: 这套Option Api 的形式和我们Vue3 compositionApi 不符,推荐下面的setup写法,更简洁
import { loginApi } from '@/api/user/index.ts';
import { pinia } from '@/store';
import { defineStore } from 'pinia';
// 使用setup模式定义
const useUserStore = defineStore(
'userStore',
() => {
// ref 和 state 对应
const nickName = ref<string>('');
const cityName = ref<string>('');
// computed 和getter 对应
const getWelcomeWords = computed(() => {
return '非常欢迎' + nickName.value + '的到来!!' || '';
});
// function 和 action 对应
async function setUserInfo() {
const res = await loginApi({ location: '苏州', key: 'e76a0884c20d49aba7bf1f9cebc0f0bc' });
console.log(res);
if (res?.code == 200) {
nickName.value = '尤雨溪';
}
}
function setCityName(name: string) {
cityName.value = name;
}
return { nickName, cityName, getWelcomeWords, setUserInfo, setCityName };
},
{
persist: {
enabled: true,
strategies: [
{
storage: localStorage, // 默认是sessionStorage
// key: 'nickName'
paths: ['nickName', 'cityName'] // 不写key: '' 或者path 数组的话 默认是全部缓存,,
}
]
}
}
);
/** 在 setup 外使用 */
export function useUserStoreHook() {
return useUserStore(pinia);
}
export default useUserStore(pinia);
(3) 页面中使用
<template>
<div class="pinia-container">
你好: {{ nickName }}
<p v-if="nickName">{{ userStore.getWelcomeWords }}</p>
<div @click="login">登录</div>
</div>
</template>
<script setup lang="ts">
import userStore from '@/store/modules/user';
import { storeToRefs } from 'pinia';
// 1、userStore.$reset() 可以重置到初始值
// 2、修改某个属性,可通过userStore.xx = ,或如下解构修改 const { nickName } = storeToRefs(userStore);
// 但解构后会丢失响应式,pinia提供了保持响应式的方法storeToRefs
// 这样nickName.value 在页面修改时会更新到全局
const { nickName } = storeToRefs(userStore);
function login() {
// userStore.setUserInfo(); // 走异步请求
userStore.setNickName(); // 虽然可userStore.nickName = XXX 直接修改,但是大项目多人合作时出问题,极难排查,store的值修改建议都走action,后期也容易维护。
}
</script>
打开调试中的application 点击登录,可以看到locationStorage 中会存储userStore 内容,实现了持久化存储的效果,若部分key 需特殊管理,在persist中单配即可
总结下,pinia 体积小,省去mutations,十分方便,一旦涉及到全局状态管理时必备。Mitt 也很优秀,兄弟组件间可以用总线通信,但别到处写。而provide inject 更适合的场景是从顶层丢出一个底下组件都用到的状态,如果想要响应式,那就传递响应式对象,子组件中watch 即可。父子组件通信,props + emit + watch 足够
六、双向绑定 v-model
父组件中
<template>
<p>父组件的年龄:{{ age }}</p>
<p>资产: {{ asset.money }}</p>
<Child v-model:age="age" v-model:asset="asset"/>
<!-- 只写v-model === v-model:modelValue -->
<!-- 原理等于: <Child :modelValue="age" @update:modelValue="age = $event" /> -->
</template>
<script setup>
import Child from './Child.vue'
import { ref } from 'vue'
const age = ref(0)
const asset = reactive({
house: 99,
money: '200亿'
});
</script>
子组件中
<template>
<!-- props.age 可以省略props -->
<p>子组件的值:{{ age }} <span @click="addAge">过去一年</span></p>
<p>资产: {{ asset.money }}</p>
</template>
<script lang="ts" setup>
const props = defineProps({
age: {
type: Number,
default: 0
},
asset: {
type: Object,
default: null
}
});
const $emit = defineEmits([ 'update:age']);
function addAge() {
$emit('update:age', props.age + 1);
// 这里有个隐藏的知识点
// props.age = props.age + 1 无效 // 如果是基本类型,必须用 emit 修改
// 但如果是引用类型,可以直接修改,切记 不可把asset 解构出来,会丢响应式,前面已提到reactive 丢失响应式的原因。一旦在修改,你一定会用let,所以多用const可以避免自己犯错!!
const asset = props.asset;
asset.money = asset.money.length > 1 ?
asset.money.substring(0, asset.money.length - 1) : 0;
}
</script>
ps:
1、和vue2 差不多,只是prop、emit 的写法有所改变,讲道理v-model 是一个语法糖,只是在父组件可以省略 @update:modelValue="age = $event" 特殊性在于父子组件都用的是同一个值
2、vue3 中引用类型或者嵌套引用类型的是可以直接修改props 来进行同步的
相信你对Vue3 组件通信有了比较全面的认识了,感谢支持,会继续更新,点赞支持喔~