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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容