在上一篇文章中,我们学习了Props传值的三种方式。今天我们来学习另外三种非常重要的组件通信方式:ref 、自定义事件 和provide/inject。这三种方式各有特色,适用于不同的场景。
一、ref实现组件通信
1.1 ref的基本概念
ref 是Vue提供的一个特殊属性,用来给元素或子组件注册引用信息。通过 $refs 可以访问到这些引用。
两种使用场景:
- 给普通DOM元素打ref → 获取该DOM元素
- 给子组件打ref → 获取该子组件的实例(vc)
1.2 父组件访问子组件
通过 ref 标记子组件,父组件可以直接访问子组件的数据 和方法。
实现步骤:
- 在子组件标签上添加
ref属性 - 在合适的时机通过
this.$refs.ref名称访问子组件实例
父组件模板:
<template>
<div>
<girl ref="girlCom" />
<button @click="getGirl">点击获取子组件</button>
</div>
</template>
父组件脚本:
export default {
methods: {
getGirl() {
// 访问子组件的方法
this.$refs.girlCom.girlFun();
// 读取子组件的数据
this.girlNum = this.$refs.girlCom.girlNum;
// 直接操作子组件数据
this.$refs.girlCom.girlNum--;
}
}
};
1.3 获取DOM元素
除了访问子组件,ref 也可以用来获取普通的DOM元素。
<template>
<div>
<h2 ref="title">sonMsg: {{ sonMsg }}</h2>
<button @click="getTitle">点击获取h2</button>
</div>
</template>
<script>
export default {
methods: {
getTitle() {
// 获取h2元素的DOM对象
console.log(this.$refs.title);
// 可以操作DOM样式
this.$refs.title.style.color = 'red';
}
}
};
</script>
1.4 注意事项
⚠️ 访问时机 :必须在组件挂载完成后 才能通过 $refs 访问。在 mounted 生命周期之后才安全。
mounted() {
// ✅ 正确:挂载完成后可以访问
console.log(this.$refs.girlCom);
}
created() {
// ❌ 错误:此时组件尚未挂载,$refs为空
console.log(this.$refs.girlCom); // undefined
}
二、自定义事件实现组件通信
2.1 子组件向父组件传递数据
自定义事件是Vue实现子传父的标准方式。
实现步骤:
- 父组件中:在子组件标签上绑定自定义事件
- 子组件中 :通过
$emit触发自定义事件并传递数据 - 父组件中:编写事件回调处理数据
父组件代码
<template>
<div>
<h1>app组件</h1>
<h3>来自girl组件的girlNum:{{ girlNum }}</h3>
<!-- 1. 绑定自定义事件getGirlNum -->
<girl @getGirlNum="getGirlNum" />
</div>
</template>
<script>
import girl from './components/girl.vue';
export default {
name: 'App',
data() {
return {
girlNum: ''
};
},
components: { girl },
methods: {
// 3. 编写事件回调,接收子组件传递的数据
getGirlNum(val, ...args) {
this.girlNum = val;
console.log('额外参数:', args);
}
}
};
</script>
子组件代码
<template>
<div class="girl">
<h2>girl组件</h2>
<h3>girlNum: {{ girlNum }}</h3>
<button @click="sendGirlNum">点击传递数据</button>
</div>
</template>
<script>
export default {
name: 'girl',
data() {
return {
girlNum: 555
};
},
methods: {
sendGirlNum() {
// 2. 触发自定义事件,传递数据
this.$emit('getGirlNum', this.girlNum, 1, 2, 3);
}
}
};
</script>
2.2 通过代码绑定自定义事件
除了在模板中绑定,还可以通过JS代码动态绑定自定义事件。这种方式更加灵活。
export default {
mounted() {
// 通过代码给子组件绑定自定义事件
this.$refs.sonCom.$on('sonFun', this.sonFun);
},
methods: {
sonFun(val) {
this.msg = val;
}
}
};
对应模板:
<template>
<div>
<son ref="sonCom" />
</div>
</template>
2.3 事件回调的this指向问题
在使用 $on 绑定事件时,需要注意回调函数的 this 指向。
| 写法 | this指向 | 说明 |
|---|---|---|
this.$on('fun', this.method) |
当前组件 | methods中的方法,this指向当前组件 |
this.$on('fun', function(){}) |
子组件 | 普通函数,this指向触发事件的组件 |
this.$on('fun', () => {}) |
当前组件 | 箭头函数,this继承外层上下文 |
// 写法一:methods中的方法,this指向当前组件
this.$refs.sonCom.$on('sonFun', this.sonFun);
// 写法二:匿名函数,this指向子组件
this.$refs.sonCom.$on('sonFun', function(val) {
this.msg = val; // this指向子组件
});
// 写法三:箭头函数,this指向当前组件
this.$refs.sonCom.$on('sonFun', (val) => {
this.msg = val; // this指向当前组件
});
三、自定义事件修饰符和解绑
3.1 事件修饰符
自定义事件也支持一些修饰符,用于控制事件的行为。
<!-- .native修饰符:绑定原生事件 -->
<girl @click.native="fun" />
<!-- .once修饰符:只触发一次 -->
<girl @fun.once="fun" />
<!-- 其他修饰符 -->
<girl @fun.prevent="fun" />
.native修饰符说明:
- 作用:让组件标签支持原生DOM事件
- 不加
.native时,@click会被视为自定义事件 - 加
.native后,@click变为原生的点击事件
3.2 解绑自定义事件
在组件销毁前,应该及时解绑自定义事件,防止内存泄漏。
<template>
<div>
<button @click="offFun">点击解绑事件</button>
</div>
</template>
<script>
export default {
methods: {
offFun() {
// 解绑单个事件
this.$off('fun');
// 解绑多个事件
this.$off(['fun', 'fun1']);
// 解绑所有事件(慎用)
this.$off();
}
}
};
</script>
解绑时机建议 :通常在 beforeDestroy 生命周期钩子中解绑。
beforeDestroy() {
// 组件销毁前解绑所有自定义事件
this.$off();
}
四、provide/inject实现跨级组件通信
4.1 解决的问题
当组件层级较深时(祖孙关系),使用Props逐级传递会变得非常繁琐。provide/inject 允许祖先组件向其所有子孙后代组件注入依赖,无论层级有多深。
App(祖先) ──provide──→ Son(中间层) ──→ Child(后代) ──→ Grandchild(后代)
↑ 无需传递 ↑ inject接收 ↑ inject接收
4.2 祖先组件提供数据
使用 provide 选项提供数据给后代组件。
export default {
data() {
return {
appNum: 123,
a: { b: 88 }
};
},
// 提供数据给后代组件
provide() {
return {
appNum: this.appNum, // 基本类型数据
appFun: this.appFun, // 方法
a: this.a // 对象类型数据
};
},
methods: {
appFun(val) {
console.log('appFun', val);
}
}
};
4.3 后代组件注入数据
使用 inject 选项注入祖先组件提供的数据。
<template>
<div>
<h4>appNum: {{ appNum }}</h4>
<button @click="appFun(childNum)">调用祖先方法</button>
</div>
</template>
<script>
export default {
// 注入祖先组件提供的数据
inject: ['appNum', 'appFun', 'a'],
data() {
return {
childNum: 222
};
}
};
</script>
4.4 数据响应性问题
provide/inject 有一个重要的限制:数据不具备完全的响应性。
| 数据类型 | 响应性表现 | 说明 |
|---|---|---|
| 基本类型 | ❌ 不具备 | 祖先修改后,后代不会自动更新 |
| 对象/数组 | ⚠️ 部分具备 | 修改内部属性可以检测到,但直接替换不会触发更新 |
解决方案:如果需要响应式的数据传递,可以考虑以下方式:
-
传递整个实例(this)
provide() {
return {
app: this // 传递整个组件实例
};
} -
使用Vuex状态管理
-
使用观察者模式手动实现响应式
五、注意事项汇总
5.1 ref相关
| 注意点 | 说明 |
|---|---|
| 访问时机 | 必须在组件挂载完成后(mounted之后)才能通过 $refs 访问 |
| 封装性 | 直接操作子组件数据会破坏组件的封装性,谨慎使用 |
| 命名冲突 | 避免 ref 名称与其他属性重复 |
5.2 自定义事件相关
| 注意点 | 说明 |
|---|---|
| 触发时机 | 避免在 mounted 中立即触发自定义事件 |
| this指向 | 注意回调函数中的 this 指向问题 |
| 事件解绑 | 在组件销毁前及时解绑自定义事件,防止内存泄漏 |
| 事件命名 | 避免使用原生事件名(如 click, change 等)作为自定义事件名 |
5.3 provide/inject相关
| 注意点 | 说明 |
|---|---|
| 数据响应性 | 基本类型数据不具备响应性 |
| 对象替换 | 直接替换对象不会触发更新,修改内部属性可以 |
| 组件耦合 | 过度使用会导致组件间耦合度增加 |
| 调试困难 | 数据来源不明确,不利于调试 |
六、通信方式总结对比
| 通信方式 | 方向 | 核心API | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|---|
| ref | 父→子 | ref + $refs |
父组件访问子组件 | 直接、简单 | 破坏组件封装性 |
| 自定义事件 | 子→父 | $emit + $on |
子组件向父组件通信 | 符合组件化思想 | 需要事件绑定和解绑 |
| provide/inject | 祖先→后代 | provide + inject |
跨级组件通信 | 解决多级传递问题 | 数据响应性有限 |
最佳实践建议
- 优先使用自定义事件实现父子组件通信,保持数据流向清晰
- 跨级组件通信考虑使用Vuex或provide/inject
- 谨慎使用ref直接操作子组件数据,尽量保持组件的独立性
- 注意事件回调的this指向,必要时使用箭头函数
- 及时解绑事件,防止内存泄漏
掌握了这几种通信方式,你已经可以应对绝大多数Vue组件通信场景。下一篇文章我们将继续学习全局事件总线和更高级的通信技巧,敬请期待!