Vue第四篇:组件通信 + DOM 更新 + 过渡动画
Vue 组件化开发中,组件之间传数据(组件通信)是核心需求,DOM 更新时机、元素动效也是前端开发高频场景。
一、自定义事件:子组件向父组件通信的优雅方式
为什么需要自定义事件?
在父组件中,我们通过props向子组件传递数据。但当子组件需要向父组件传递数据时,就需要自定义事件了。
核心作用
自定义事件是子组件给父组件传数据的专属方式(只能子传父)。原理很简单:父组件给子组件绑定一个自定义事件,子组件触发这个事件并传递数据,事件的回调函数写在父组件里,就能轻松拿到子组件的数据。
两种绑定方式对比
方式1:直接在模板中绑定(简洁)
<!-- 父组件 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绑定(更灵活)
<!-- 父组件 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>自定义事件完整生命周期
// 子组件内部 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)
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)
<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)
<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. 安装依赖
npm i pubsub-js 2. 接收数据的组件(src/components/School.vue)
<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)
<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让输入框显示,想立刻让输入框聚焦:
// 错误写法: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)
/* 自定义动画样式,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>包裹元素
<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:
<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和自定义事件
<!-- 清晰的数据流 --><Child :data="parentData" @update="handleUpdate" />2. 谨慎使用ref直接操作
// 尽量避免 this.$refs.child.doSomething() // 优先通过事件通信 this.$refs.child.$emit('do-something')3. 事件总线要记得清理
// 必须的!防止内存泄漏 beforeDestroy(){ this.$bus.$off('event-name', this.handler)}4. 合理使用$nextTick
// 只在需要操作更新后的DOM时使用 this.data = newValue this.$nextTick(()=>{ // 这里DOM已经更新 })总结
核心要点回顾
- 自定义事件:子组件向父组件通信的标准方式
- 全局事件总线:任意组件间通信的轻量级方案
- 消息订阅发布:跨框架通信的备选方案
- $nextTick:处理DOM异步更新的关键方法过渡动画:使用Vue内置组件实现平滑的UI效果