Vue 组件化开发中,组件之间传数据(组件通信)是核心需求,DOM 更新时机、元素动效也是前端开发高频场景。
一、自定义事件:子组件向父组件通信的优雅方式
为什么需要自定义事件?
在父组件中,我们通过props向子组件传递数据。但当子组件需要向父组件传递数据时,就需要自定义事件了。
核心作用
自定义事件是子组件给父组件传数据的专属方式(只能子传父)。原理很简单:父组件给子组件绑定一个自定义事件,子组件触发这个事件并传递数据,事件的回调函数写在父组件里,就能轻松拿到子组件的数据。
两种绑定方式对比
方式1:直接在模板中绑定(简洁)
bash
<!-- 父组件 App.vue -->
<template>
<div>
<!-- 通过@绑定自定义事件 -->
<ChildComponent @child-event="handleChildEvent" />
</div>
</template>
<script>
export default {
methods: {
handleChildEvent(data) {
console.log('收到子组件数据:', data)
}
}
}
</script>
<!-- 子组件 ChildComponent.vue -->
<template>
<button @click="sendData">发送数据给父组件</button>
</template>
<script>
export default {
methods: {
sendData() {
this.$emit('child-event', {
message: 'Hello from child!',
timestamp: new Date()
})
}
}
}
</script>
方式2:使用ref绑定(更灵活)
bash
<!-- 父组件 App.vue -->
<template>
<div>
<!-- 使用ref获取组件引用 -->
<ChildComponent ref="childRef" />
</div>
</template>
<script>
export default {
methods: {
handleChildEvent(data) {
console.log('收到子组件数据:', data)
}
},
mounted() {
// 在组件挂载后绑定事件
this.$refs.childRef.$on('child-event', this.handleChildEvent)
// 如果只需要触发一次
// this.$refs.childRef.$once('child-event', this.handleChildEvent)
},
beforeDestroy() {
// 组件销毁前解绑事件,防止内存泄漏
this.$refs.childRef.$off('child-event')
}
}
</script>
自定义事件完整生命周期
bash
// 子组件内部
export default {
methods: {
// 触发事件(发送数据)
sendMessage() {
this.$emit('message', 'Hello!')
},
// 解绑单个事件
unbindSingle() {
this.$off('message')
},
// 解绑多个事件
unbindMultiple() {
this.$off(['message', 'other-event'])
},
// 解绑所有事件
unbindAll() {
this.$off()
}
}
}
核心总结
| 操作 | 具体写法 |
|---|---|
| 绑定自定义事件 | 方式 1:<Demo @事件名="回调函数"/>(@是 v-on 简写) 方式 2:this.$refs.子组件.$on('事件名', 回调) |
| 触发自定义事件 | this.$emit('事件名', 要传递的数据) |
| 解绑自定义事件 | this.$off('事件名')(单个)/ this.$off([事件1,事件2])(多个)/ this.$off()(全部) |
| 只触发一次 | 方式 1:<Demo @事件名.once="回调"/> 方式 2:this.$refs.子组件.$once('事件名', 回调) |
重要注意事项
- 组件上绑定原生 DOM 事件(比如 click),要加native修饰符,否则会被当成自定义事件:
<Student @click.native="handleClick"/>; - 用
$refs绑定事件时,回调函数要么写在methods里,要么用箭头函数(如this.$refs.student.$on('jojo', (name) => { console.log(name) })),否则this会指向子组件,而非父组件!
二、全局事件总线:任意组件间的通信桥梁
核心作用
自定义事件只能实现 "子传父",而全局事件总线 能实现任意组件间通信(父传子、子传父、兄弟组件传),是 Vue 中最常用的跨组件通信方式,本质就是一个所有组件都能访问的 "全局对象"。
核心条件
全局事件总线必须满足 2 个要求:
- 所有组件都能访问到这个对象(挂载到 Vue 原型上);
- 这个对象要有
$on(绑定事件)、$emit(触发事件)、$off(解绑事件)方法(Vue 实例 / 组件实例自带这些方法)。
1. 安装全局事件总线(src/main.js)
bash
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false // 关闭生产提示
new Vue({
el: '#app',
render: h => h(App),
// 关键:在beforeCreate钩子中安装全局事件总线
beforeCreate() {
// 把Vue实例(this)挂载到Vue原型上,命名为$bus
// 所有组件都能通过this.$bus访问这个全局对象
Vue.prototype.$bus = this;
}
})
2. 接收数据的组件(src/components/School.vue)
bash
<template>
<div class="school">
<h2>学校名称:{{ name }}</h2>
<h2>学校地址:{{ address }}</h2>
</div>
</template>
<script>
export default {
name: 'School',
data() {
return {
name: '尚硅谷',
address: '北京'
}
},
methods: {
// 接收数据的回调函数
demo(data) {
console.log('我是School组件,收到了Student组件的数据:', data);
}
},
mounted() {
// 绑定全局事件:事件名demo,回调函数demo
this.$bus.$on('demo', this.demo);
},
beforeDestroy() {
// 组件销毁前解绑事件,避免内存泄漏
this.$bus.$off('demo');
}
}
</script>
3. 发送数据的组件(src/components/Student.vue)
bash
<template>
<div class="student">
<h2>学生姓名:{{ name }}</h2>
<h2>学生性别:{{ sex }}</h2>
<button @click="sendStudentName">把学生名给School组件</button>
</div>
</template>
<script>
export default {
name: 'Student',
data() {
return {
name: '张三',
sex: '男'
}
},
methods: {
sendStudentName() {
// 触发全局事件demo,把学生名传递过去
this.$bus.$emit('demo', this.name);
}
}
}
</script>
使用步骤
- 安装总线 :在
main.js的 Vue 实例中,beforeCreate钩子里写Vue.prototype.$bus = this; - 接收数据 :A 组件想收数据 → A 组件
mounted钩子中执行this.$bus.$on('事件名', 回调函数); - 发送数据 :B 组件想发数据 → B 组件中执行
this.$bus.$emit('事件名', 要传的数据); - 解绑事件 :A 组件beforeDestroy钩子中执行
this.$bus.$off('事件名')(必做,避免内存泄漏)
三、消息订阅与发布:另一种全局通信方案
核心作用
和全局事件总线功能一致,也是任意组件间通信 ,但需要借助第三方库pubsub-js实现。日常开发中用得少,因为全局事件总线已能满足需求,无需额外安装依赖。
为什么选择pubsub?
虽然Vue有事件总线,但在某些场景下,你可能需要:
- 更精细的控制(取消特定订阅)
- 跨框架通信(与非Vue组件通信)
- 使用已有的第三方库生态
完整案例(Student→School 通信)
1. 安装依赖
bash
npm i pubsub-js
2. 接收数据的组件(src/components/School.vue)
bash
<template>
<div class="school">
<h2>学校名称:{{ name }}</h2>
<h2>学校地址:{{ address }}</h2>
</div>
</template>
<script>
// 引入pubsub-js库
import pubsub from 'pubsub-js'
export default {
name: 'School',
data() {
return {
name: '尚硅谷',
address: '北京'
}
},
methods: {
// 回调函数:第一个参数是消息名,第二个才是真正的数据
demo(msgName, data) {
console.log('我是School组件,收到了数据:', data);
}
},
mounted() {
// 订阅消息:消息名demo,回调demo,返回订阅ID(用于取消订阅)
this.pubId = pubsub.subscribe('demo', this.demo);
},
beforeDestroy() {
// 取消订阅(必须传订阅ID)
pubsub.unsubscribe(this.pubId);
}
}
</script>
3. 发送数据的组件(src/components/Student.vue)
bash
<template>
<div class="student">
<h2>学生姓名:{{ name }}</h2>
<h2>学生性别:{{ sex }}</h2>
<button @click="sendStudentName">把学生名给School组件</button>
</div>
</template>
<script>
// 引入pubsub-js库
import pubsub from 'pubsub-js'
export default {
name: 'Student',
data() {
return {
name: 'JOJO',
sex: '男'
}
},
methods: {
sendStudentName() {
// 发布消息:消息名demo,要传递的数据
pubsub.publish('demo', this.name);
}
}
}
</script>
使用步骤
- 安装:npm i pubsub-js;
- 引入:import pubsub from 'pubsub-js';
- 订阅消息(收数据):this.pubId = pubsub.subscribe('消息名', 回调函数);
- 发布消息(发数据):pubsub.publish('消息名', 要传的数据);
- 取消订阅:pubsub.unsubscribe(this.pubId)(组件销毁前执行)。
事件总线 vs pubsub
| 特性 | Vue事件总线 | pubsub-js |
|---|---|---|
| 依赖 | Vue实例 | 独立库 |
| 语法 | this.$bus.$emit / $on |
publish / subscribe |
| 取消订阅 | $off | unsubscribe |
| 适用范围 | Vue组件间 | 任意JS环境 |
| 学习成本 | 低(Vue自带) | 低(简单API) |
四、$nextTick:等待DOM更新的神器
为什么需要$nextTick?
Vue的数据更新是异步的。当你修改数据后,DOM并不会立即更新,而是进入一个队列,等待下一个"tick"(时机)【所有数据更新完】统一更新。
$nextTick 能让代码 "等 DOM 更新完成后再执行",避免拿到旧的 DOM 节点。
场景
比如修改isShow让输入框显示,想立刻让输入框聚焦:
bash
// 错误写法:DOM还没更新,找不到输入框,会报错
this.isShow = true;
this.$refs.input.focus();
// 正确写法:用$nextTick等DOM更新完再聚焦
this.isShow = true;
this.$nextTick(() => {
this.$refs.input.focus(); // 成功聚焦!
});
核心总结
| 项 | 说明 |
|---|---|
| 语法 | 方式 1:this.$nextTick(回调函数) 方式 2:await this.$nextTick()(返回 Promise) |
| 作用 | 下次 DOM 更新循环结束后执行回调函数 |
| 使用场景 | 修改数据后,需要操作更新后的 DOM(比如聚焦输入框、获取新 DOM 的高度 / 宽度) |
五、过渡与动画:让页面动起来
核心作用
Vue 封装的<transition>/<transition-group>组件,能给 "插入 / 更新 / 移除 DOM 元素" 的过程添加动效,不用自己写复杂的 CSS 动画逻辑。
基础用法(单个元素动效)
1. 编写动画样式(CSS)
bash
/* 自定义动画样式,name是hello,所以样式前缀为hello- */
/* 进入动画:起点 → 过程 → 终点 */
.hello-enter {
opacity: 0; /* 进入起点:透明 */
transform: translateX(100px); /* 进入起点:右移100px */
}
.hello-enter-active {
transition: all 0.5s ease; /* 进入过程:0.5秒过渡 */
}
.hello-enter-to {
opacity: 1; /* 进入终点:不透明 */
transform: translateX(0); /* 进入终点:回到原位 */
}
/* 离开动画:起点 → 过程 → 终点 */
.hello-leave {
opacity: 1; /* 离开起点:不透明 */
transform: translateX(0); /* 离开起点:原位 */
}
.hello-leave-active {
transition: all 0.5s ease; /* 离开过程:0.5秒过渡 */
}
.hello-leave-to {
opacity: 0; /* 离开终点:透明 */
transform: translateX(-100px); /* 离开终点:左移100px */
}
2. 用<transition>包裹元素
bash
<template>
<div>
<button @click="isShow = !isShow">显示/隐藏</button>
<!-- transition包裹要加动效的元素,name对应CSS样式前缀 -->
<transition name="hello">
<h1 v-show="isShow">你好啊!</h1>
</transition>
</div>
</template>
<script>
export default {
data() {
return {
isShow: true // 控制元素显示/隐藏
}
}
}
</script>
3. 多元素动效(<transition-group>)
如果有多个元素需要加动效,必须用<transition-group>,且每个元素要指定唯一key:
bash
<template>
<div>
<button @click="addItem">添加元素</button>
<!-- transition-group包裹多元素,必须加key -->
<transition-group name="hello">
<div v-for="(item, index) in list" :key="index" v-show="item.show">
{{ item.text }}
</div>
</transition-group>
</div>
</template>
<script>
export default {
data() {
return {
list: [{ text: '第一个元素', show: true }, { text: '第二个元素', show: true }]
}
},
methods: {
addItem() {
this.list.push({ text: `新元素${Date.now()}`, show: true });
}
}
}
</script>
核心总结
| 项 | 写法 / 说明 |
|---|---|
| 单个元素 | 用<transition name="自定义名">包裹,CSS 样式前缀对应 name 值 |
| 多个元素 | 用<transition-group>包裹,每个元素必须加key |
| 动画样式 | 进入:v-enter /v-enter-active/v-enter-to 离开:v-leave /v-leave-active/v-leave-to(v 会被 name 替换) |
六、通信方案选择指南
如何选择合适的通信方式?
| 场景 | 推荐方案 | 说明 |
|---|---|---|
| 父子组件通信 | props + 自定义事件 | Vue推荐的标准方式 |
| 父调用子方法 | ref | 直接访问子组件实例 |
| 兄弟组件通信 | 全局事件总线 | 通过共同父组件中转或使用事件总线 |
| 跨多层组件 | 全局事件总线 / Vuex | 避免props逐层传递 |
| 非Vue组件间 | pubsub | 与其他JS库/框架通信 |
| 简单状态共享 | 事件总线 | 小型项目快速实现 |
| 复杂状态管理 | Vuex | 中大型项目,需要状态追踪 |
最佳实践建议
1. 优先使用props和自定义事件
bash
<!-- 清晰的数据流 -->
<Child :data="parentData" @update="handleUpdate" />
2. 谨慎使用ref直接操作
bash
// 尽量避免
this.$refs.child.doSomething()
// 优先通过事件通信
this.$refs.child.$emit('do-something')
3. 事件总线要记得清理
bash
// 必须的!防止内存泄漏
beforeDestroy() {
this.$bus.$off('event-name', this.handler)
}
4. 合理使用$nextTick
bash
// 只在需要操作更新后的DOM时使用
this.data = newValue
this.$nextTick(() => {
// 这里DOM已经更新
})
总结
核心要点回顾
- 自定义事件:子组件向父组件通信的标准方式
- 全局事件总线:任意组件间通信的轻量级方案
- 消息订阅发布:跨框架通信的备选方案
- $nextTick:处理DOM异步更新的关键方法过渡动画:使用Vue内置组件实现平滑的UI效果