Vue3组件通信完全指南:9大方案从基础到高阶,告别Prop地狱!

6 阅读5分钟

【props】

概述

props是一种常用的通信方式,常用于父组件->子组件

代码实现

父组件:

<template>
    <Child :hobby="hobby" :job="job"></Child>
    <p>{{like}}</p>
</template>

<script setup>
import { ref } from 'vue';
import Child from './components/child.vue';
let hobby = '我爱打台球'
let job = '软件工程师'
</script>

<style scoped>

</style>

子组件:

<template>
    <h2>爱好:{{ hobby }}</h2>
    <h2>职业:{{ job }}</h2>
    <button @click="ziChuanFu">点击一下</button>
</template>

<script setup>
import { defineProps , ref } from 'vue'
// 定义props方式一
const props = defineProps({
    hobby: {
        type: String,
        default: '喜欢旅游'
    }
})
// 定义props方式二
const props = defineProps(['hobby','job'])

</script>

<style scoped></style>

【emit】

概述

emit是自定义事件,用于子组件->父组件

代码实现

父组件:

<template>
    <Child @sendLike="getLike"></Child>
    <p>{{like}}</p>
</template>

<script setup>
import { ref } from 'vue';
import Child from './components/child.vue';
let like = ref('')
const getLike = (val) => {
    like.value = val
}
</script>

<style scoped>

</style>

子组件:

<template>
    <button @click="ziChuanFu">点击一下</button>
</template>

<script setup>
import { defineEmits, ref } from 'vue'
let like = ref('篮球')
const emits = defineEmits('ziChuanFu')
const ziChuanFu = () => {
    emits('sendLike',like)
}

</script>

<style scoped></style>

【v-model】

概述

v-model可以实现父子组件相互通信

组件标签上v-model的本质是:modelValue + update:modelValue事件

代码实现

父组件:

<template>
    <p>父组件项目已运行:{{day}}天</p>
    <button @click="addParentDay">点击加一下</button>
    <Child v-model:day="day"></Child>
</template>

<script setup>
import { ref } from 'vue';
import Child from './components/child.vue';
let day = ref(1)
const addParentDay = () => {
    day.value += 1
}
</script>

<style scoped>

</style>

子组件:

<template>
    <p>子组件项目已运行:{{ day }}天</p>
    <button @click="addChildDay">点击加一下</button>
</template>

<script setup>
const props = defineProps(['day'])
const emit = defineEmits(['update:day']); //其中day是指父组件的标签中的day
const addChildDay = () => {
    emit('update:day', props.day + 1);
}
</script>

<style scoped></style>

【mitt】

概述

mitt可以用于任意组件间的通信(包括非父子组件、兄弟组件)

安装mitt

npm i mitt

新建文件

src\utils\emitter.ts

// 引入mitt 
import mitt from "mitt";

// 创建emitter
const emitter = mitt()

/*
  // 绑定事件
  emitter.on('abc',(value)=>{
    console.log('abc事件被触发',value)
  })
  emitter.on('xyz',(value)=>{
    console.log('xyz事件被触发',value)
  })

  setInterval(() => {
    // 触发事件
    emitter.emit('abc',666)
    emitter.emit('xyz',777)
  }, 1000);

  setTimeout(() => {
    // 清理事件
    emitter.all.clear()
  }, 3000); 
*/

// 创建并暴露mitt
export default emitter

接受数据的组件:

<template>
    <p>{{like}}</p>
</template>

<script setup>
import { ref,onUnmounted } from 'vue';
import emitter from './utils/mitt';
let like = ref('')
// 绑定事件
emitter.on('getLike',(val)=>{
  like.value = val
})
onUnmounted(()=>{
  // 解绑事件
  emitter.off('getLike')
})
</script>

<style scoped>

</style>

提供数据的组件:

<template>
    <button @click="handleClick">点击一下</button>
</template>

<script setup>
import { ref } from 'vue'
import emitter from '../utils/mitt';
let like = ref('篮球')
const handleClick = () => {
    emitter.emit('getLike',like.value)
}
</script>

<style scoped></style>

注:使用mitt作为组件通信的时候,必须要导出共享实例

【$attrs】

概述

$attrs用于实现当前组件的父组件,向当前组件的子组件通信(祖→孙)。

具体说明:$attrs是一个对象,包含所有父组件传入的标签属性。

注意:`$attrs`会自动排除`props`中声明的属性(可以认为声明过的 `props` 被子组件自己“消费”了)

代码实现

父组件:

<template>
  <div class="father">
    <h3>父组件</h3>
    <Child :a="a" :b="b" :c="c" :d="d" v-bind="{x:100,y:200}" :updateA="updateA"/>
  </div>
</template>

<script setup name="Father">
import Child from './Child.vue'
import { ref } from "vue";
let a = ref(1)
let b = ref(2)
let c = ref(3)
let d = ref(4)

function updateA(value){
    a.value = value
}
</script>

子组件:

<template>
    <div class="child">
        <h3>子组件</h3>
        <GrandChild v-bind="$attrs"/>
    </div>
</template>

<script setup name="Child">
import GrandChild from './GrandChild.vue'
</script>

孙组件:

<template>
    <div class="grand-child">
        <h3>孙组件</h3>
        <h4>a:{{ a }}</h4>
        <h4>b:{{ b }}</h4>
        <h4>c:{{ c }}</h4>
        <h4>d:{{ d }}</h4>
        <h4>x:{{ x }}</h4>
        <h4>y:{{ y }}</h4>
        <button @click="updateA(666)">点我更新A</button>
    </div>
</template>

<script setup name="GrandChild">
defineProps(['a','b','c','d','x','y','updateA'])
</script>

【$refs、parent】

  1. 概述:

    • $refs用于 :父→子。
    • $parent用于:子→父。
  2. 原理如下:

    属性说明
    $refs值为对象,包含所有被ref属性标识的DOM元素或组件实例。
    $parent值为对象,当前组件的父组件实例对象。

【provide、inject】

概述

可以实现祖孙组件直接通信

具体使用

   在祖先组件中通过`provide`配置向后代组件提供数据
   在后代组件中通过`inject`配置来声明接收数据

代码实现

【第一步】父组件中,使用`provide`提供数据

<template>
  <div class="father">
    <h3>父组件</h3>
    <h4>钱包余额:{{ money }}</h4>
    <h4>帕拉梅拉:{{ car }}</h4>
    <button @click="money += 1">钱包余额+1</button>
    <button @click="car.price += 1">豪车价格+1</button>
    <Child/>
  </div>
</template>
    
<script setup name="Father">
import Child from './Child.vue'
import { ref,reactive,provide } from "vue";
let money = ref(100)
let car = reactive({
brand:'奔驰',
price:100
})
// 用于更新money的方法
function updateMoney(value){
money.value += value
}
// 提供数据
provide('moneyContext',{money,updateMoney})
provide('car',car)
</script>
注意:子组件中不用编写任何东西,是不受到任何打扰的

【第二步】孙组件中使用inject配置项接受数据。

<template>
  <div class="grand-child">
    <h3>孙组件</h3>
    <h4>资产:{{ money }}</h4>
    <h4>汽车:{{ car }}</h4>
    <button @click="updateMoney(6)">点我</button>
  </div>
</template>

<script setup name="GrandChild">
import { inject } from 'vue';
// 注入数据
let {money,updateMoney} = inject('moneyContext',{
    money:0,
    updateMoney => {}
})
let car = inject('car')
</script>

【pinia】

安装 pinia

`npm install pinia`

在main.js中引入

import { createApp } from 'vue'
import App from './App.vue'

/* 引入createPinia,用于创建pinia */
import { createPinia } from 'pinia'

/* 创建pinia */
const pinia = createPinia()
const app = createApp(App)

/* 使用插件 */{}
app.use(pinia)
app.mount('#app')

这时开发者工具中已经有了pinia选项(小菠萝标志)

image.png

新建str/store/user.js

// 引入defineStore用于创建store
import { defineStore } from 'pinia'

// 定义并暴露一个store
export const useUserInfo = defineStore('userInfo', {
    // 动作
    actions: {
        changeUsername(value) {
            this.username = value
        }
    },
    // 状态
    state() {
        return {
            username: '',
            sex: '男',
        }
    },
    // 计算
    getters: {}
})

父组件:

<template>
  <h1>父组件用户名:{{ userStore.username }}</h1>
  <button @click="updateName">修改子组件用户名</button>
  <Child></Child>
</template>

<script setup>
import Child from './components/child.vue'
import { useUserInfo } from './store/user.js'
const userStore = useUserInfo()

const updateName = () => {
  userStore.changeUsername('张三')  // 调用 action 修改状态
}
</script>

<style scoped>
</style>

子组件:

<template>
    <h2>子组件用户名{{ userStore.username }}</h2>
    <button @click="updateName">修改父组件用户名</button>
</template>

<script setup>
import { useUserInfo } from '../store/user.js'
const userStore = useUserInfo()
const updateName = () => {
     userStore.changeUsername('李四')  // 调用 action 修改状态
}
</script>

<style scoped></style>

插槽

默认插槽

概述

默认插槽(Default Slot)  是一种组件插槽机制,允许父组件向子组件传递内容,并在子组件的指定位置渲染这些内容

代码实现

父组件:

<template>
<Child title="今日热门歌曲">
  <ul>
    <li v-for="item in musicList" :key="item.id">{{ item.name }}</li>
  </ul>
</Child>
</template>

<script setup>
import { ref } from 'vue'
import Child from './components/child.vue';

const musicList = ref([
  { id: 1, name: '歌曲1' },
  { id: 2, name: '歌曲2' }
])
</script>

子组件:

<template>
  <div class="item">
    <h3>{{ title }}</h3>
    <!-- 默认插槽 -->
    <slot></slot>
  </div>
</template>

<script setup>
defineProps({
  title: String
})
</script>

具名插槽

概述

具名插槽是 Vue 3 中一种允许你在组件中定义多个插槽位置,并可以精确控制内容分发到特定位置的机制。与默认插槽不同,具名插槽通过名称来区分不同的插槽区域

代码实现

父组件:

<template>
    <Child title="今日热门歌曲">
        <template #s1>
            <ul>
            <li v-for="item in musicList" :key="item.id">{{ item.name }}</li>
            </ul>
        </template>
        <template #s2>
            <a href="/music" target="_blank">更多</a>
        </template>
    </Child>
</template>

<script setup>
import { ref } from 'vue'
import Child from './components/child.vue';

const musicList = ref([
  { id: 1, name: '歌曲1' },
  { id: 2, name: '歌曲2' }
])
</script>

子组件:

<template>
  <div class="item">
    <h3>{{ title }}</h3>
    <slot name="s1"></slot>
    <slot name="s2"></slot>
  </div>
</template>

<script setup>
defineProps({
  title: String
})
</script>

作用域插槽

概述

数据在组件的自身,但根据数据生成的结构需要组件的使用者来决定

代码实现

父组件:

<template>
    <Music v-slot="params">
        <ul>
            <li v-for="item in params.musicList" :key="item.id">{{ item.name }}</li>
        </ul>
    </Music>
</template>

<script setup>
import Music from './components/child.vue'
</script>

<style scoped>
</style>

子组件:

<template>
    <div class="category">
        <h2>今日流行歌曲</h2>
        <slot :musicList="musicList"></slot>
    </div>
</template>

<script setup name="Music">
import {reactive} from 'vue'
let musicList = reactive([
  {id:'01',name:'富士山下'},
  {id:'02',name:'寂寞烟火'},
  {id:'03',name:'一路生花'},
  {id:'04',name:'花海'}
])
</script>