Vue组件间通信
Vue组件间的通信可以通过多种方式实现:
- props 和 emit
- refs (引用组件或元素实例)
- 通过插槽 (slot) 传递内容
- Event Bus (使用 Vue 实例作为中央事件总线)
- Vuex (使用集中状态管理)
- parent 和 children (直接访问组件实例)
- attrs 和 listeners (传递属性和事件)
- provide 和 inject (祖先与后代组件间通信)
- 全局状态 (如: window.globalState)
- localStorage/sessionStorage
- 自定义事件 (在 DOM 中)
- 通过直接修改 props (不推荐)
对比表格
通信方法 | 用途 | 说明 |
---|---|---|
props 和 emit | 父传子、子传父 | 父组件通过props传递数据,子组件通过emit回传消息 |
refs | 父传子 | 父组件通过refs直接访问子组件实例 |
插槽 (slot) | 父传子 | 父组件插入内容到子组件模板中 |
Event Bus | 兄弟间通信 | 使用Vue实例作为事件总线来发布和订阅事件 |
Vuex | 全局通信 | 用于集中状态管理,所有组件都可以访问和修改状态 |
parent 和 children | 父传子、子传父 | 通过 p a r e n t 或 parent或 parent或children直接访问组件实例 |
attrs 和 listeners | 父传子、子传父 | 非prop的属性和事件的传递 |
provide 和 inject | 祖先与后代间 | 祖先组件提供变量,后代组件注入 |
window.globalState | 全局通信 | 全局变量,任何组件都可以访问和修改 |
localStorage/sessionStorage | 全局通信 | 数据持久化存储,在多个组件间共享状态 |
自定义事件 | 兄弟间通信 | 使用DOM的事件系统 |
直接修改 props | 父传子 | 不推荐使用,可能导致不可预测的行为 |
1. props和emit
父组件 -> 子组件: 通过 props
传递数据。
子组件 -> 父组件: 通过 $emit
触发父组件的方法。
ParentComponent.vue
<template>
<div>
<ChildComponent :parentData="data" @childEvent="handleChildEvent"></ChildComponent>
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: {
ChildComponent
},
data() {
return {
data: "Data from Parent"
}
},
methods: {
handleChildEvent(payload) {
console.log("Received data from child:", payload);
}
}
}
</script>
ChildComponent.vue
<template>
<div>
<p>{{ parentData }}</p>
<button @click="sendToParent">Send Data to Parent</button>
</div>
</template>
<script>
export default {
props: {
parentData: {
type: String,
required: true
}
},
methods: {
sendToParent() {
this.$emit("childEvent", "Data from Child");
}
}
}
</script>
2. ref
通过引用来调用子组件的方法或访问其数据。
ParentComponent.vue
<template>
<div>
<ChildComponent ref="childRef"></ChildComponent>
<button @click="useChildRef">Use Child Ref</button>
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: {
ChildComponent
},
methods: {
useChildRef() {
// 使用 $refs 调用子组件的方法
this.$refs.childRef.childMethod();
}
}
}
</script>
ChildComponent.vue
<template>
<div>
<!-- 子组件内容 -->
</div>
</template>
<script>
export default {
methods: {
childMethod() {
console.log("Child method called from parent!");
}
}
}
</script>
3. 通过插槽 (slot) 传递内容
这是一个更灵活的方式,允许你在父组件中定义一些默认内容,而子组件则可以选择是否使用这些内容。
ParentComponent.vue:
<template>
<div>
<ChildComponent>
<template v-slot:default>
{{ message }}
</template>
</ChildComponent>
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: { ChildComponent },
data() {
return {
message: 'Hello from parent!'
};
}
}
</script>
ChildComponent.vue:
<template>
<div>
<slot></slot> <!-- 这里显示父组件传入的内容 -->
</div>
</template>
<script>
export default {}
</script>
使用插槽,你可以灵活地分发父组件的内容,使得组件复用更为简单和灵活。
基础插槽
ChildComponent.vue
<template>
<div>
<slot></slot> <!-- 默认插槽 -->
</div>
</template>
ParentComponent.vue
<template>
<ChildComponent>
Hello from Parent! <!-- 这段内容会被放入 ChildComponent 的 <slot></slot> 中 -->
</ChildComponent>
</template>
具名插槽
有时,你可能希望子组件有多个插槽,并且每个插槽都有其特定的名称。
ChildComponent.vue
<template>
<div>
<header>
<slot name="header"></slot> <!-- header 插槽 -->
</header>
<main>
<slot></slot> <!-- 默认插槽 -->
</main>
<footer>
<slot name="footer"></slot> <!-- footer 插槽 -->
</footer>
</div>
</template>
ParentComponent.vue
<template>
<ChildComponent>
<template v-slot:header>
This is the header.
</template>
This is the default content.
<template v-slot:footer>
This is the footer.
</template>
</ChildComponent>
</template>
作用域插槽
有时,子组件希望将一些数据“传递”回父组件。这可以通过作用域插槽实现。
子组件 (ChildComponent.vue
):
<template>
<div>
<slot myMessage="Hello from child!"></slot> <!-- 提供给插槽的数据 -->
</div>
</template>
ParentComponent.vue
<template>
<ChildComponent>
<template v-slot:default="slotProps">
{{ slotProps.myMessage }}
</template>
</ChildComponent>
</template>
在上述示例中,子组件提供了一个myMessage
属性,父组件可以通过插槽的slotProps
来访问这个属性。
4. Event Bus
Event Bus 是一个使用 Vue 实例作为中央事件总线的模式。通过它,不相关的组件可以进行通信。
EventBus.js
首先创建一个 EventBus。
import Vue from 'vue';
export const EventBus = new Vue();
ParentComponent.vue
在父组件中,你可以监听来自子组件的事件。
<template>
<div>
<ChildComponent></ChildComponent>
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
import { EventBus } from './EventBus.js';
export default {
components: {
ChildComponent
},
created() {
// 监听子组件发送的事件
EventBus.$on('childEvent', this.handleChildEvent);
},
beforeDestroy() {
// 销毁监听器,避免内存泄漏
EventBus.$off('childEvent', this.handleChildEvent);
},
methods: {
handleChildEvent(payload) {
console.log("Received data from child via EventBus:", payload);
}
}
}
</script>
ChildComponent.vue
在子组件中,你可以发送事件到 EventBus。
<template>
<div>
<button @click="sendToParent">Send Data to EventBus</button>
</div>
</template>
<script>
import { EventBus } from './EventBus.js';
export default {
methods: {
sendToParent() {
// 使用 EventBus 发送事件
EventBus.$emit('childEvent', 'Data from Child via EventBus');
}
}
}
</script>
5. Vuex
Vuex 是一个状态管理库,用于 Vue.js 应用程序。通过 Vuex, 任何组件都可以访问或更改状态。
安装Vuex
首先得安装Vuex
npm install vuex --save
这里注意安装vuex的版本,如果是vue2需要写成:
npm install vuex@3 --save
store.js
设置 Vuex store。
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export default new Vuex.Store({
state: {
message: ''
},
mutations: {
// 设定消息
setMessage(state, message) {
state.message = message;
}
}
});
main.js
import Vue from 'vue'
import App from './App.vue'
import store from './store' // 从你的store.js导入
Vue.config.productionTip = false
new Vue({
store, // 这里
render: h => h(App),
}).$mount('#app')
ParentComponent.vue
在父组件中,你可以设置并从 Vuex store 中获取数据。
<template>
<div>
<ChildComponent></ChildComponent>
<p>Message from Vuex: {{ messageFromVuex }}</p>
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: {
ChildComponent
},
computed: {
// 从 Vuex store 获取数据
messageFromVuex() {
return this.$store.state.message;
}
}
}
</script>
ChildComponent.vue
在子组件中,你可以更改 Vuex store 的状态。
<template>
<div>
<button @click="sendMessage">Send Data to Vuex</button>
</div>
</template>
<script>
export default {
methods: {
sendMessage() {
// 使用 mutation 更改 Vuex store 的状态
this.$store.commit('setMessage', 'Data from Child to Vuex');
}
}
}
</script>
6. parent 和 children
通过直接访问组件实例来实现通信。
ParentComponent.vue
<template>
<div>
<ChildComponent></ChildComponent>
<button @click="callChildMethod">Call Child Method</button>
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: {
ChildComponent
},
methods: {
parentMethod() {
console.log("Parent method called from child!");
},
callChildMethod() {
// 调用子组件的方法
this.$children[0].childMethod();
}
}
}
</script>
ChildComponent.vue
<template>
<div>
<button @click="callParentMethod">Call Parent Method</button>
</div>
</template>
<script>
export default {
methods: {
childMethod() {
console.log("Child method called from parent!");
},
callParentMethod() {
// 调用父组件的方法
this.$parent.parentMethod();
}
}
}
</script>
7. attrs 和 listeners
用于在组件间传递属性和监听器。
ParentComponent.vue
<template>
<div>
<p>Parent Color: {{ parentColor }}</p>
<ChildComponent :color="parentColor" @changeColor="changeColor"/>
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: {
ChildComponent
},
data() {
return {
parentColor: 'red'
}
},
methods: {
changeColor(newColor) {
this.parentColor = newColor;
}
}
}
</script>
ChildComponent
<template>
<div
v-bind="$attrs"
:style="{ backgroundColor: color }"
@click="changeMyColor"
v-on="$listeners">
Click me to change color
</div>
</template>
<script>
export default {
props: {
color: {
type: String,
default: ''
}
},
methods: {
changeMyColor() {
let newColor = this.color === 'red' ? 'blue' : 'red';
this.$emit('changeColor', newColor);
}
}
}
</script>
attrs:当父组件传递给子组件的属性没有在子组件的props定义时,它们会被视为“绑定的属性”并存在于$attrs中。
listeners:它包含了传递给子组件的事件监听器。
在以上例子中,父组件向子组件传递一个属性,并监听子组件触发一个事件。
- 父组件传递给子组件一个color属性,其值为parentColor。
- 当子组件内部div被点击时,changeMyColor方法被调用,它改变颜色并通过$emit发送一个自定义事件changeColor。
- 这个changeColor事件被父组件监听,并执行内部的changeColor方法来更新parentColor的值。
扩展对比
我们来设计一个简单的 BaseInput
组件作为例子。这个组件旨在替代普通的 <input>
元素,并带有一些自定义样式或功能。
1. 使用 attrs
和 listeners
BaseInput.vue
<template>
<!-- 使用$attrs来透传任何传入的属性(除props外) -->
<!-- 使用$listeners来透传所有传入的事件监听器 -->
<input v-bind="$attrs" v-on="$listeners" class="custom-input">
</template>
<script>
export default {
inheritAttrs: false, // 防止Vue将属性绑定到根元素
props: {
value: String // 作为例子,我们仅定义了一个value prop
}
}
</script>
在这个设置中,你可以传递任何属性和监听器到 BaseInput
,而不需要在其内部明确定义。例如:
<BaseInput type="text" placeholder="Enter text" @input="handleInput" />
2. 不使用 attrs
和 listeners
BaseInput.vue
<template>
<!-- 必须为每一个可能的属性和事件明确定义绑定和监听 -->
<input :type="type" :placeholder="placeholder" @input="onInput" class="custom-input">
</template>
<script>
export default {
props: {
value: String,
type: {
type: String,
default: 'text'
},
placeholder: String // 必须为每一个可能的属性定义一个prop
},
methods: {
onInput(event) {
// 对于事件,我们需要手动emit
this.$emit('input', event.target.value);
}
}
}
</script>
在这个设置中,每当你需要传递一个新的属性或监听器到 BaseInput
,你必须在其内部明确定义。例如,如果你想添加一个 maxlength
属性,你需要更新组件的 props
。如果你想监听 focus
事件,你需要在组件内部添加一个新的方法。
总结:
- 使用
$attrs
和$listeners
的版本可以透传任何未明确定义为props
的属性和所有事件监听器,使得组件更加灵活和通用。 - 不使用它们的版本要求你为每一个可能的属性和事件在组件内部明确定义,这样会使组件的维护和扩展变得更加困难。
8. provide 和 inject
用于祖先组件向其所有后代组件提供变量。
ParentComponent.vue
<template>
<div>
<ChildComponent></ChildComponent>
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
provide() {
return {
message: 'Message from Parent'
};
},
components: {
ChildComponent
}
}
</script>
ChildComponent.vue
<template>
<div>
{{ message }}
</div>
</template>
<script>
export default {
inject: ['message']
}
</script>
9. 全局状态
使用全局变量实现组件间通信是一个简单但不推荐的方法,因为它违反了组件的封装原则,容易导致状态管理的混乱。但在某些简单的场景或快速原型开发中,它确实可以很快地实现组件间的通信。
在提供的例子中,我们在 main.js
里创建了一个全局变量 globalState
,然后在任何 Vue 组件中都可以访问它。
让我们举一个例子来说明如何在两个组件之间使用这种方法进行通信。
1. 设置全局状态
main.js
window.globalState = { message: 'Hello from Global!' };
2. 创建一个组件修改这个全局状态
UpdateGlobalMessage.vue
<template>
<button @click="updateMessage">Update Global Message</button>
</template>
<script>
export default {
methods: {
updateMessage() {
window.globalState.message = "Updated Global Message!";
console.log(window.globalState.message)
}
},
created() {
console.log(window.globalState.message)
}
}
</script>
现在,当你点击 “Update Global Message” 按钮时,全局状态的 message
会被更新,这个更新会立即反映在console
上。
尽管这种方法简单明了,但在大型应用中可能会导致很多问题。状态的来源和修改可能变得难以追踪,而且容易产生意外的副作用。对于大型应用,推荐使用专门的状态管理库,如 Vuex,以更有组织、可预测的方式管理状态。
10. localStorage/sessionStorage
使用 Web Storage API 来存储和检索数据。
ParentComponent.vue
<template>
<div>
{{ retrievedMessage }}
<ChildComponent @message-stored="refreshComponent"></ChildComponent>
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: {
ChildComponent
},
data() {
return {
retrievedMessage: localStorage.getItem('message') || 'Default Message'
}
},
methods: {
refreshComponent() {
this.retrievedMessage = localStorage.getItem('message')
this.$forceUpdate()
}
}
}
</script>
ChildComponent.vue
<template>
<div>
<button @click="storeMessage">Store Message</button>
</div>
</template>
<script>
export default {
methods: {
storeMessage() {
localStorage.setItem('message', 'Stored Message');
this.$emit('message-stored')
}
}
}
</script>
11. 自定义事件
使用 DOM 的自定义事件机制来实现组件通信。
ParentComponent.vue
<template>
<div @custom-message="handleMessage">
<ChildComponent />
<p>{{ message }}</p>
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
name: 'ParentComponent',
components: {
ChildComponent
},
data() {
return {
message: 'I havent received any information yet'
};
},
methods: {
handleMessage(event) {
this.message = event.detail;
console.log("Received custom meessage event:", event.detail)
}
}
}
</script>
ChildComponent.vue
<template>
<button @click="sendToParent">click me</button>
</template>
<script>
export default {
name: 'ChildComponent',
methods: {
sendToParent() {
// 创建自定义事件
const event = new CustomEvent('custom-message', { detail: 'This is information from the chlidComponent' , bubbles: true});
// 触发该事件
this.$el.dispatchEvent(event);
}
}
}
</script>
12. 通过直接修改 props (不推荐)
ParentComponent.vue:
<template>
<div>
<ChildComponent :message="message"/>
<button @click="changeMessage">Change Message</button>
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: { ChildComponent },
data() {
return {
message: 'Hello from parent!'
};
},
methods: {
changeMessage() {
// 不建议这样做!
this.$children[0].message = 'Changed message!';
}
}
}
</script>
ChildComponent.vue:
<template>
<div>
{{ message }}
</div>
</template>
<script>
export default {
props: ['message']
}
</script>
虽然上述方法可以工作,但直接修改子组件的props
可能会导致不可预测的行为,特别是当子组件依赖props
来触发某些生命周期钩子或计算属性时。