Vue3.2组件间通信

组件之间的传值及通信

一、第一种场景 两层组件之间传值和监听值改变

// 父组件
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 组件通信有了比较全面的认识了,感谢支持,会继续更新,点赞支持喔~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容