Vue 中 8 种组件通信方式
vue 中常见的 8 种常规通信方案
- 通过 props 传递
- 通过 $emit 触发自定义事件
- 使用 ref
- EventBus
- $parent 或 $root
- attts 和 listeners
- Provide 与 Inject
- Vuex 和 Pinia
组件间通信的分类可以分成以下:
- 父子关系组件选择 props 与 $emit ,或者 ref
- 兄弟关系组件选择 $bus 和 $parent
- 祖先与后代关系组件选择 attrs 与 listeners 或 Provide 与 Inject
- 复杂关系组件选择 vuex 或 Pinia
1. 父子组件通信 (Props / Emit / ref)
1.1. Props(父 -> 子)
Props 是 Vue 中实现父组件向子组件传递数据的核心方式,它遵循了单向数据流的原则。
具体实现分为两步:
- 父组件 (传递方):在模板的子组件标签上,通过属性绑定(使用 : 或 v--bind)的方式,将父组件自身的数据传递下去。
- 子组件 (接收方):在 <script setup> 中,使用 defineProps 宏来声明并接收从父组件传递来的 props。
子组件:
<script setup lang='js'>import { ref } from 'vue'import Child from '../../components/Child.vue'const data = ref('父组件数据')
</script><template><Child :data="data" />
</template>
父组件:
<script setup lang='js'>const props = defineProps({data: {type: String,default: ''}})
</script><template><div class=''>Child {{ props.data }}</div>
</template>
1.2. emit(子 -> 父)
子组件向父组件通信,主要使用 emit 机制。首先,在子组件的 <script setup> 中,必须使用 defineEmits 来声明它将要触发的自定义事件名。然后,在需要的时候,调用 defineEmits 返回的 emit 函数,触发该事件并可以附带任意参数。在父组件中,通过在子组件标签上使用 @ 符号(即 v-on)来监听这个自定义事件,并绑定一个处理函数。当事件被触发时,这个处理函数就会被调用,并且其参数列表会按顺序接收到子组件 emit 出来的所有值,从而实现了自下而上的数据传递。
子组件:
<script setup lang='js'>const emit = defineEmits(['custom-event'])const handleClick = () => {emit('custom-event', '子组件传值')}
</script><template><button @click="handleClick">点击传值</button>
</template>
父组件:
<script setup lang='js'>import Child from '../../components/Child.vue'const handleCustomEvent = (value) => {console.log(value)}
</script><template><Child :data="data" @custom-event="handleCustomEvent" />
</template>
1.3. Ref
1.3.1. 父 -> 子
通过 ref 实现父组件向子组件的命令式通信:首先,在父组件的模板中,为子组件标签添加 ref 属性以获取其实例;然后,在父组件的逻辑中,通过访问这个 ref 实例直接调用子组件暴露的方法,并将需要传递的数据作为方法参数传入。子组件对应的方法在接收到参数后,即可执行相应操作。
父组件:
<script setup lang='js'>import { ref } from 'vue'import Child from '../../components/Child.vue'const data = ref('父组件传递的数据')const ChildRef = ref(null)const handleChild = () => {ChildRef.value.init(data)}
</script><template><div class=''><Child ref="ChildRef" /><button @click="handleChild">传递数据</button></div>
</template>
子组件:
<script setup lang='js'>import { ref } from 'vue'import GrandChild from './GrandChild.vue'let data = ref('')const init = (value) => {data.value = valueconsole.log(value)}defineExpose({ init })
</script><template><div class=''>Child<GrandChild /></div>
</template>
1.3.2. 子 -> 父
在 Vue 3 中,父组件可以通过 ref 来获取子组件的实例,并主动调用子组件暴露的方法或访问其数据。
这个过程分为两步:
- 子组件(被动方):在 <script setup> 中,子组件必须使用 defineExpose 宏,显式地将其希望被父组件访问的属性和方法暴露出去,形成一个公开的 API。
- 父组件(主动方):在父组件模板中,通过在子组件标签上设置 ref="childRef",并将 childRef 关联到一个 ref() 对象,即可获取到子组件的实例。之后,父组件就可以在适当的时机(如 onMounted 或事件回调中),通过 childRef.value 来访问子组件暴露的 API。
子组件:
<script setup lang='js'>import { ref } from 'vue'import GrandChild from './GrandChild.vue'let ChildData = ref('子组件的数据')defineExpose({ ChildData })
</script><template><div class=''>Child<GrandChild /></div>
</template>
父组件:
<script setup lang='js'>import { onMounted } from 'vue'import Child from '../../components/Child.vue'onMounted(() => {if (ChildRef.value) {console.log(ChildRef.value.ChildData);}})
</script><template><div class=''><Child ref="ChildRef" /></div>
</template>
2. 祖孙/跨级组件通信 (Provide / Inject)
provide 和 inject 是 Vue 官方推荐的跨级组件通信方案。它允许一个祖先组件 (Provider) 作为依赖的提供者,将其数据或方法提供 (provide) 给其后代组件树中的任何一个组件。
最核心的优势在于,这个过程具有“穿透性”。无论组件层级有多深,任何中间层组件都无需进行任何额外的 props 声明或传递。最终的后代组件 (Injector) 只需要通过 inject 并使用与提供者相同的 key,就能精准地接收到这份依赖。
简单来说:祖先组件使用 provide 给孙子传递组件,中间层什么都不需要做,在孙子组件使用 inject 接收
祖先组件:
<script setup lang='js'>import { ref, provide, readonly } from 'vue'import Child from '../../components/Child.vue'const data = ref('祖先数据')provide('data', data) // readonly(data) 使用 readonly 包裹代表只读,孙子不可修改</script><template><div class=''><Child /></div>
</template>
孩子(中间层):
<script setup lang='js'>import GrandChild from './GrandChild.vue'
</script><template><div class=''>Child <GrandChild /></div>
</template>
孙子组件:
<script setup lang='js'>import { inject } from 'vue';let data = inject('data');console.log("祖先传递过来的数据:", data.value);</script><template><div class=''>{{ data }}</div>
</template>
3. 祖孙组件通信($attrs 与 listeners)
孙子向祖先通信,是通过事件透传实现的:祖先在父组件上监听事件,父组件通过 v-bind="$attrs"
将该监听器透传给孙子,最终由孙子组件 emit 触发。
祖先向孙子传值,是一个自上而下的数据流:祖先传递的属性,父组件通过 v-bind="$attrs"
,最终被孙子组件的 props 接收,完美解决了“属性逐层传递”的问题。
祖先组件:
<script setup lang='js'>import { ref, onMounted } from 'vue'import Child from '../../components/Child.vue'// 准备要传递的数据const messageFromGrand = ref('来自祖先的问候!');const userFromGrand = ref({ name: '张三' });// 准备一个方法来处理来自孙子组件的响应const handleChildResponse = (dataFromChild) => {console.log('祖先组件收到了来自孙子的响应:', dataFromChild);console.log(`祖先收到了来自 ${dataFromChild.from} 的消息: ${dataFromChild.text}`);};</script><template><div class="grandparent-component"><h1>我是祖先组件 (Grandparent)</h1><Child parentTitle="父组件的专属标题" :message="messageFromGrand" :user="userFromGrand" @response="handleChildResponse" /></div>
</template><style scoped>.grandparent-component {border: 2px solid #3498db;padding: 20px;}
</style>
父组件:
<script setup lang='js'>
import { ref } from 'vue'
import GrandChild from './GrandChild.vue'// 1. 【关键】Parent 组件只声明它自己真正关心的 prop
const props = defineProps({parentTitle: {type: String,required: true}
});</script><template><div class='parent-component'><h2>我是父组件 (Parent)</h2><p>我只关心我自己的标题: <strong>{{ props.parentTitle }}</strong></p><p>其他来自祖先的东西,我直接透传给 Child...</p><!-- 2. 【核心】使用 v-bind="$attrs"$attrs 对象包含了所有从 Grandparent 传递过来,但没有被 Parent 的 props(即 parentTitle) 接收的“剩余”属性和事件。v-bind="$attrs" 会将这些属性和事件监听器,原封不动地应用到 Child 组件上。--><GrandChild v-bind="$attrs" /></div>
</template><style scoped>
.parent-component {border: 2px solid #e67e22;padding: 20px;margin-top: 20px;
}
</style>
孙子组件:
<script setup>
// 1. 声明接收来自祖先的 props
const props = defineProps({message: {type: String,default: '默认消息'},user: {type: Object,default: () => ({ name: '默认用户' })}
});// 2. 声明会触发的事件
const emit = defineEmits(['response']);const handleChildClick = () => {// 3. 触发事件,并回传数据const responseData = { from: 'Child', text: '你好,祖先!' };emit('response', responseData);
};
</script><template><div class="child-component"><h3>我是孙子组件 (Child)</h3><p>接收到的消息: <strong>{{ props.message }}</strong></p><p>接收到的用户: <strong>{{ props.user.name }}</strong></p><button @click="handleChildClick">点击我,向祖先响应</button></div>
</template><style scoped>
.child-component {border: 2px solid #42b983;padding: 20px;margin-top: 20px;
}
</style>
4. EventsBus
main.js
import Vue from 'vue'
Vue.prototype.$bus = new Vue()
发送:
this.$bus.$emit('custom-event', 'hello')
接收:
this.$bus.$on('custom-event', msg => {console.log('接收到:', msg)
})
5. root
传递方:
<template><Child />
</template><script setup>import { getCurrentInstance } from 'vue'import Child from '../../components/Child.vue'const app = getCurrentInstance()app.appContext.config.globalProperties.$rootMsg = "来自 $root 的全局信息"
</script>
接收方:
<template><div><p>孙组件获取的 root 信息:{{ rootMsg }}</p></div>
</template><script setup>
import { getCurrentInstance } from 'vue'const app = getCurrentInstance()
const rootMsg = app.appContext.config.globalProperties.$rootMsg
console.log(rootMsg);
</script>