Vue2 开发中,组件间的数据传递是核心技能。根据组件间的层级关系,通信方式主要分为父子组件传值、兄弟组件传值及跨层级组件传值。本文将系统梳理这些传值方案,并结合实现原理进行深度解析。
父子组件传值
父子组件关系由模板的嵌套使用决定。例如,在模板中使用了 Child 组件,它们之间便构成了父子关系。
<template>
<div id="app">
<Child/>
</div>
</template>
<script>
import Child from './components/Child.vue';
export default {
name: 'App',
components: {
Child
}
}
</script>
父子组件间的数据流是单向的,具体可分为父传子和子传父两种情况。
父组件向子组件传值
父组件向子组件传值使用 v-bind(或其语法糖 :)传递数据,子组件通过 props 选项接收。props 支持类型检查、默认值设置和必填项验证。
父组件:
<Child :message="sendMsg" :obj="sendObj" />
<script>
...
data() {
return {
sendMsg: "sendxxxx",
sendObj: {
name: "zhangsan",
age: 18
}
}
}
...
</script>
子组件:
<div>{{ message }} {{ obj }}</div>
...
<script>
props: {
message: {
type: String,
required: true
},
obj: {
type: Object
}
}
</script>
页面将显示接收到的值。这种传递是响应式的,当父组件数据变化时,子组件会同步更新。
sendxxxx { "name": "zhangsan", "age": 18 }
子组件向父组件传值
子组件通过 $emit 触发自定义事件,父组件使用 v-on(或其语法糖 @)监听事件来接收数据。事件可以传递任意类型的数据。
子组件:
data() {
return {
transmitObj: {
msg: "夏天夏天悄悄过去留下小秘密"
},
transmitString: "压心底压心底不要告诉你"
}
},
created() {
this.$emit("transmitObj", this.transmitObj)
this.$emit("transmitStr", this.transmitString)
}
父组件:
<Child
@transmitObj="handleTransmitObj"
@transmitStr="handleTransmitStr"
/>
...
<script>
methods: {
handleTransmitObj(params) {
console.log("Child组件传递对象:", params);
},
handleTransmitStr(params) {
console.log("Child组件传递字符串", params);
}
}
</script>

兄弟组件间传值
兄弟组件间通信主要有两种思路:通过共同的父组件中转,或使用事件总线(Event Bus)。第一种方式本质上是父子通信的组合,不再赘述。重点介绍事件总线模式。
事件总线 (Event Bus)
事件总线是一种基于发布/订阅模式的简易全局通信方案。在 Vue2 中,可以利用 Vue 实例内置的 $on(订阅)、$emit(发布)和 $off(取消订阅)方法来实现。
首先,在项目入口文件(如 main.js)中创建并全局挂载一个事件总线实例:
// main.js
export const bus = new Vue()
Vue.prototype.$bus = bus;
你可能会好奇,为什么事件总线是一个 Vue 实例?这是因为每个 Vue 实例都自带了事件系统相关的 $on、$emit、$off 方法,其底层原理我们稍后探讨。先看用法,假设有两个兄弟组件:
Child组件(发布事件):
mounted(){
this.$bus.$emit("msg", “夏天夏天”);
}
Brother组件(订阅事件):
created(){
this.$bus.$on('msg', res => {
console.log(“接收消息”, res)
})
},
beforeDestroy(){
// 组件销毁前移除监听,避免内存泄漏
this.$bus.$off('msg')
}
通过以上代码,兄弟组件间就能完成通信。深入了解其背后的 JavaScript 事件机制,我们来看 Vue 源码是如何实现这三个核心方法的。
每个 Vue 实例在初始化时都会创建一个内部事件仓库 _events:
// src/core/instance/events.js
export function initEvents (vm: Component) {
vm._events = Object.create(null) // 空对象,用于存储事件
vm._hasHookEvent = false
}
-
$on 方法 (订阅)
当调用 $on('msg', fn) 时,回调函数 fn 会被存储到 _events 对象中,与事件名建立映射。
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
const vm: Component = this
if (Array.isArray(event)) { // 处理传入事件名数组的情况
event.forEach(e => vm.$on(e, fn))
} else {
;(vm._events[event] || (vm._events[event] = [])).push(fn)
}
return vm
}
-
$emit 方法 (发布)
当调用 $emit('msg', data) 时,会从 _events 中取出对应事件名的回调函数队列并依次执行。
Vue.prototype.$emit = function (event: string): Component {
const vm: Component = this
let cbs = vm._events[event]
if (cbs) {
const args = toArray(arguments, 1) // 获取$emit除事件名外的参数
for (let i = 0; i < cbs.length; i++) {
try {
cbs[i].apply(vm, args) // 执行回调
} catch (e) {
handleError(e, vm, `event handler for "${event}"`)
}
}
}
return vm
}
-
$off 方法 (取消订阅)
用于移除事件监听器,是防止内存泄漏的关键。
Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
const vm: Component = this;
// 如果不传参数,移除所有事件监听
if (!arguments.length) {
vm._events = Object.create(null);
return vm;
}
// 如果event是数组,遍历处理
if (Array.isArray(event)) {
event.forEach(e => vm.$off(e, fn));
return vm;
}
const cbs = vm._events[event];
if (!cbs) {
return vm;
}
// 如果只传了事件名,移除该事件所有监听
if (arguments.length === 1) {
vm._events[event] = null;
return vm;
}
// 如果指定了具体回调函数,则只移除该函数
if (fn) {
vm._events[event] = cbs.filter(cb => cb !== fn && cb.fn !== fn);
}
return vm;
};
provide / inject
provide 和 inject 是 Vue 提供的依赖注入 API,主要用于祖先组件向任意深度的后代组件传递数据或方法,无需通过 props 逐层传递。在 Vue2 中,默认情况下注入的值不是响应式的,若需响应性需要传递一个响应式对象(如父组件 data 中的对象)。
其典型使用场景是全局配置,如主题、语言、用户信息等。对于需要集中管理、频繁变化的复杂应用状态,更推荐使用专业的 前端框架 状态管理库,如 Vuex 或 Pinia。