Vue第四篇:组件通信 + DOM 更新 + 过渡动画

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('事件名', 回调)

重要注意事项

  1. 组件上绑定原生 DOM 事件(比如 click),要加native修饰符,否则会被当成自定义事件:<Student @click.native="handleClick"/>
  2. $refs绑定事件时,回调函数要么写在methods里,要么用箭头函数(如this.$refs.student.$on('jojo', (name) => { console.log(name) })),否则this会指向子组件,而非父组件!

二、全局事件总线:任意组件间的通信桥梁

核心作用

自定义事件只能实现 "子传父",而全局事件总线 能实现任意组件间通信(父传子、子传父、兄弟组件传),是 Vue 中最常用的跨组件通信方式,本质就是一个所有组件都能访问的 "全局对象"。

核心条件

全局事件总线必须满足 2 个要求:

  1. 所有组件都能访问到这个对象(挂载到 Vue 原型上);
  2. 这个对象要有$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>

使用步骤

  1. 安装总线 :在main.js的 Vue 实例中,beforeCreate钩子里写Vue.prototype.$bus = this
  2. 接收数据 :A 组件想收数据 → A 组件mounted钩子中执行this.$bus.$on('事件名', 回调函数)
  3. 发送数据 :B 组件想发数据 → B 组件中执行this.$bus.$emit('事件名', 要传的数据)
  4. 解绑事件 :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>

使用步骤

  1. 安装:npm i pubsub-js;
  2. 引入:import pubsub from 'pubsub-js';
  3. 订阅消息(收数据):this.pubId = pubsub.subscribe('消息名', 回调函数);
  4. 发布消息(发数据):pubsub.publish('消息名', 要传的数据);
  5. 取消订阅: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效果
相关推荐
向下的大树2 小时前
VUE父子组件传参中的触发时机问题:异步场景下的解决方案
前端·javascript·vue.js
英俊潇洒美少年2 小时前
vue2中使用节流防抖函数时,使用的vue状态始终是初始化的数据
前端·javascript·vue.js
刘一说2 小时前
Vue3响应式原理重构:从Object.defineProperty到Proxy的革命性升级
javascript·vue.js·重构
切糕师学AI3 小时前
Vue 中的生命周期钩子
前端·javascript·vue.js
暴富暴富暴富啦啦啦3 小时前
使用 v-html 仅渲染新数据的方法
前端·javascript·vue.js
林_xi3 小时前
二次封装一个vue3签字板signature pad
前端·javascript·vue.js
w***76553 小时前
vue2和vue3的区别
前端·javascript·vue.js
奔跑的web.3 小时前
TypeScript 泛型完全指南:写法、四大应用场景与高级用法
前端·javascript·vue.js·typescript
SevgiliD3 小时前
文本溢出省略并Tooltip组件在表单和表格内的使用
前端·javascript·vue.js