一文搞懂 Vue 组件通信,附详细代码案例

一、前言

在 Vue.js 的开发世界里,组件通信就像是各个 "岛屿" 之间的桥梁,连接着不同的功能模块,让数据得以顺畅流通,交互得以无缝实现。无论是简单的小型项目,还是复杂的大型应用,组件之间高效、准确的通信都是构建流畅用户体验的关键所在。作为一名 C 编程博主,深入理解 Vue 组件通信,不仅能拓宽技术视野,更能在实际项目中优化代码结构、提升开发效率。接下来,就让我们一同探索 Vue 组件通信的奇妙世界。

二、父子组件通信

(一)props 传值

父子组件通信最常见的方式之一便是通过 props 来实现父组件向子组件传值。props 可以理解为父组件给子组件的 "礼物",让子组件能够拥有父组件所提供的数据,从而展示不同的内容或执行特定的逻辑。

在父组件中,使用子组件时,通过类似 HTML 属性的方式传递数据:

xml 复制代码
<template>
  <ChildComponent :message="parentMessage" />
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
  components: { ChildComponent },
  data() {
    return {
      parentMessage: "这是来自父组件的消息"
    };
  }
};
</script>

在子组件中,需要显式地用 props 选项来声明它所接收的属性:

xml 复制代码
<template>
  <div>{{ message }}</div>
</template>
<script>
export default {
  props: ['message']
};
</script>

这里有几个要点需要注意:

  • props 是单向数据流,意味着子组件不能直接修改 props 中的数据,这样能保证数据流向的可预测性,避免数据混乱。若子组件需要基于 props 的值进行修改操作,应通过触发父组件的事件,让父组件来完成数据的更新。
  • props 的数据类型可以是基础类型(如字符串、数字、布尔值等),也可以是引用类型(如对象、数组)。对于引用类型的数据,虽然子组件不能直接修改 props 本身,但可以修改其内部的属性值,不过这依然可能带来一些潜在的问题,所以在操作时需谨慎,尽量遵循单向数据流原则。

(二)$emit 触发事件

当子组件需要向父组件传递消息,告知父组件某些事情发生时,就轮到 $emit 登场了。它就像是子组件向父组件发出的 "信号弹",让父组件能够及时知晓并做出相应反应。

假设子组件中有个按钮,点击按钮后要向父组件传递一个自定义事件及相关数据:

xml 复制代码
<template>
  <div>
    <button @click="sendMessage">点击向父组件传值</button>
  </div>
</template>
<script>
export default {
  methods: {
    sendMessage() {
      this.$emit('childMessage', '这是子组件传来的数据');
    }
  }
};
</script>

在父组件中,使用子组件时,通过 v-on(简写为 @)监听子组件触发的自定义事件:

xml 复制代码
<template>
  <div>
    <ChildComponent @childMessage="handleChildMessage" />
  </div>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
  components: { ChildComponent },
  methods: {
    handleChildMessage(data) {
      console.log('收到子组件消息:', data);
    }
  }
};
</script>

如此一来,子组件就能与父组件进行有效的交互,实现数据向上传递,让父组件能够根据子组件的状态变化来更新自身或执行其他操作。

(三)v-model 与.sync 修饰符

v-model 在 Vue 组件通信中占据着极为重要的地位,它为我们在表单元素或组件上创建双向数据绑定提供了便捷的方式,本质上是 v-bind 和 v-on 的语法糖。

在父组件中使用 v-model 绑定数据:

xml 复制代码
<template>
  <ChildComponent v-model="parentData" />
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
  components: { ChildComponent },
  data() {
    return {
      parentData: ''
    };
  }
};
</script>

在子组件中,需要通过 model 选项来声明 prop 和对应的事件:

xml 复制代码
<template>
  <input :value="value" @input="$emit('input', $event.target.value)" />
</template>
<script>
export default {
  model: {
    prop: 'value',
    event: 'input'
  },
  props: {
    value: String
  }
};
</script>

这样,父组件和子组件之间的数据就能实现双向同步,用户在子组件中修改数据,父组件中的数据也会随之更新,反之亦然。

.sync 修饰符同样用于实现类似双向绑定的效果,它更多地用于对组件的某个 prop 进行 "双向" 更新操作,比如组件的 loading 状态、子菜单的展开状态等。

父组件使用 .sync 修饰符:

xml 复制代码
<template>
  <ChildComponent :title.sync="parentTitle" />
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
  components: { ChildComponent },
  data() {
    return {
      parentTitle: '初始标题'
    };
  }
};
</script>

子组件通过 $emit 触发更新事件:

xml 复制代码
<template>
  <div>{{ title }}</div>
  <button @click="updateTitle">更新标题</button>
</template>
<script>
export default {
  props: {
    title: String
  },
  methods: {
    updateTitle() {
      this.$emit('update:title', '新标题');
    }
  }
};
</script>

总的来说,v-model 主要针对表单类组件的双向数据绑定,语义上更侧重于最终的操作结果;而 .sync 修饰符更灵活,能用于多个 prop 的类似双向更新场景,侧重于状态的互相传递。

(四)ref 特性

ref 特性就像是给组件或 DOM 元素贴上的一个 "专属标签",父组件可以借此精准地访问子组件实例或者 DOM 元素,进而获取子组件的数据或者调用子组件的方法。

在父组件的模板中,给子组件添加 ref 属性:

xml 复制代码
<template>
  <ChildComponent ref="childComponentRef" />
  <button @click="callChildMethod">调用子组件方法</button>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
  components: { ChildComponent },
  methods: {
    callChildMethod() {
      if (this.$refs.childComponentRef) {
        this.$refs.childComponentRef.childMethod();
      }
    }
  }
};
</script>

在子组件中,需要通过 defineExpose(Vue 3)或者将方法挂载到 this 上(Vue 2)来暴露给父组件访问:

xml 复制代码
<template>
  <div>子组件内容</div>
</template>
<script setup>
import { defineExpose } from 'vue';
function childMethod() {
  console.log('子组件方法被调用');
}
defineExpose({ childMethod });
</script>

需要注意的是,$refs 并不是响应式的,它只会在组件渲染完成后生效,所以不要在模板或计算属性中过度依赖它进行数据绑定,应将其作为一种直接操作子组件的 "应急通道",谨慎使用。

(五) children

<math xmlns="http://www.w3.org/1998/Math/MathML"> p a r e n t 和 parent 和 </math>parent和children 这两个属性为组件间通信提供了一种直接的层级访问方式。 <math xmlns="http://www.w3.org/1998/Math/MathML"> p a r e n t 能让子组件访问父组件实例,仿佛子组件沿着家族树向上找到它的"家长";而 parent 能让子组件访问父组件实例,仿佛子组件沿着家族树向上找到它的 "家长";而 </math>parent能让子组件访问父组件实例,仿佛子组件沿着家族树向上找到它的"家长";而children 则允许父组件获取当前实例的直接子组件,就像家长了解自己的孩子一样。

在子组件中访问父组件的属性或调用父组件的方法:

xml 复制代码
<template>
  <div>
    <button @click="callParentMethod">调用父组件方法</button>
  </div>
</template>
<script>
export default {
  methods: {
    callParentMethod() {
      if (this.$parent) {
        this.$parent.parentMethod();
      }
    }
  }
};
</script>

在父组件中访问子组件:

xml 复制代码
<template>
  <div>
    <ChildComponent />
    <button @click="callChildrensMethods">调用子组件方法</button>
  </div>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
  components: { ChildComponent },
  methods: {
    callChildrensMethods() {
      this.$children.forEach(child => {
        if (child.childMethod) {
          child.childMethod();
        }
      });
    }
  }
};
</script>

不过,使用 <math xmlns="http://www.w3.org/1998/Math/MathML"> p a r e n t 时要格外小心,因为过度依赖它会使组件之间的耦合性变强,不利于组件的复用和维护。当父组件结构发生变化时,依赖 parent 时要格外小心,因为过度依赖它会使组件之间的耦合性变强,不利于组件的复用和维护。当父组件结构发生变化时,依赖 </math>parent时要格外小心,因为过度依赖它会使组件之间的耦合性变强,不利于组件的复用和维护。当父组件结构发生变化时,依赖parent 的子组件可能会出现错误,所以尽量在组件关系相对稳定且简单的场景下谨慎使用。

三、非父子组件通信

(一)EventBus(事件总线)

在 Vue 的组件江湖里,并非只有父子组件之间才有 "交流" 的需求,兄弟组件或者其他非父子关系的组件同样时常需要互通有无。EventBus 就像是一个热闹集市中的 "传声筒",让组件之间能够轻松传递消息。

它的原理其实是利用了 Vue 实例的事件机制,我们创建一个空的 Vue 实例作为中央事件总线,其他组件通过在这个实例上监听( <math xmlns="http://www.w3.org/1998/Math/MathML"> o n )和触发( on)和触发( </math>on)和触发(emit)事件来实现通信。

假设我们有两个兄弟组件,组件 A 和组件 B,组件 B 需要向组件 A 传递一些数据:

首先,创建一个 EventBus.js 文件,实例化一个空 Vue 实例作为事件总线:

javascript 复制代码
import Vue from 'vue';
export const EventBus = new Vue();

在组件 B 中,引入 EventBus 并触发事件来传递数据:

xml 复制代码
<template>
  <div>
    <button @click="sendDataToA">向组件A发送数据</button>
  </div>
</template>
<script>
import { EventBus } from './EventBus.js';
export default {
  methods: {
    sendDataToA() {
      const data = '这是来自组件B的数据';
      EventBus.$emit('dataFromB', data);
    }
  }
};
</script>

在组件 A 中,引入 EventBus 并监听对应的事件来接收数据:

xml 复制代码
<template>
  <div>
    接收组件B的数据:{{ receivedData }}
  </div>
</template>
<script>
import { EventBus } from './EventBus.js';
export default {
  data() {
    return {
      receivedData: null
    };
  },
  mounted() {
    EventBus.$on('dataFromB', (data) => {
      this.receivedData = data;
    });
  }
};
</script>

如此一来,兄弟组件之间就能实现数据的传递,这种方式简单直接,在一些小型项目或者简单场景下非常实用。不过需要注意的是,在组件销毁时,最好手动移除监听的事件,防止内存泄漏,比如在组件 A 的 beforeDestroy 钩子函数中:

xml 复制代码
<script>
export default {
  //...其他代码
  beforeDestroy() {
    EventBus.$off('dataFromB');
  }
};
</script>

(二)provide /inject

当组件层级像一棵大树一样层层嵌套,深处的子孙组件渴望获取祖先组件的数据时,provide / inject 就如同大树的 "脉络",为数据的传递开辟了一条绿色通道。祖先组件可以通过 provide 选项提供数据,子孙组件则利用 inject 选项注入这些数据,实现跨层级的通信。

想象一个场景,有一个根组件 App.vue,它下面嵌套了多层组件,最底层的组件 GrandChild.vue 需要获取根组件中的一些配置信息:

在 App.vue 中:

xml 复制代码
<template>
  <div>
    <ChildComponent />
  </div>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
  components: { ChildComponent },
  provide() {
    return {
      appConfig: {
        themeColor: 'blue',
        apiUrl: 'https://api.example.com'
      }
    };
  }
};
</script>

在 GrandChild.vue 中:

xml 复制代码
<template>
  <div>
    当前主题颜色:{{ themeColor }},API地址:{{ apiUrl }}
  </div>
</template>
<script>
export default {
  inject: ['appConfig'],
  computed: {
    themeColor() {
      return this.appConfig.themeColor;
    },
    apiUrl() {
      return this.appConfig.apiUrl;
    }
  }
};
</script>

这种方式使得数据能够跨越多个层级直接传递到需要的组件手中,避免了层层通过 props 传递的繁琐。但要注意,provide 提供的数据默认是非响应式的,如果需要响应式的数据传递,可以使用 Vue.observable 或者在 Vue 3 中使用 ref、reactive 等响应式 API 来包装数据。

(三)Vuex 状态管理

在大型的 Vue 项目中,当组件之间的状态变得错综复杂,如同一张庞大的蜘蛛网,普通的组件通信方式可能会让代码陷入混乱的泥沼。此时,Vuex 就像是一位专业的 "管家",登场来管理全局的状态。

Vuex 是专门为 Vue.js 应用程序开发的状态管理模式,它采用集中式存储来管理应用的所有组件的状态。通过定义 state(存储状态)、mutations(同步修改状态)、actions(异步操作,通常用于调用 mutations)、getters(类似于计算属性,用于获取状态的派生值)等核心概念,让组件间的数据流动变得清晰可控。

例如,我们有一个电商项目,多个组件都需要共享用户的购物车信息:

首先,安装和配置 Vuex:

npm install vuex

在 store/index.js 文件中:

javascript 复制代码
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export default new Vuex.Store({
  state: {
    cartItems: []
  },
  mutations: {
    addToCart(state, item) {
      state.cartItems.push(item);
    },
    removeFromCart(state, itemId) {
      state.cartItems = state.cartItems.filter(item => item.id!== itemId);
    }
  },
  actions: {
    addItemToCart({ commit }, item) {
      commit('addToCart', item);
    },
    removeItemFromCart({ commit }, itemId) {
      commit('removeFromCart', itemId);
    }
  },
  getters: {
    cartTotalPrice(state) {
      return state.cartItems.reduce((total, item) => total + item.price * item.quantity, 0);
    }
  }
});

在组件中,比如商品详情页组件 ProductDetail.vue,用户点击加入购物车按钮时:

xml 复制代码
<template>
  <div>
    <h2>{{ product.name }}</h2>
    <p>价格:{{ product.price }}</p>
    <button @click="addToCart">加入购物车</button>
  </div>
</template>
<script>
import { mapActions } from 'vuex';
export default {
  computed: {
    product() {
      // 假设这里获取到当前商品信息
      return { id: 1, name: '商品1', price: 99 };
    }
  },
  methods: {
   ...mapActions(['addItemToCart']),
    addToCart() {
      this.addItemToCart(this.product);
    }
  }
};
</script>

在购物车组件 Cart.vue 中,展示购物车商品列表和总价:

xml 复制代码
<template>
  <div>
    <h2>购物车</h2>
    <ul>
      <li v-for="item in cartItems" :key="item.id">
        {{ item.name }} - 数量:{{ item.quantity }} - 总价:{{ item.price * item.quantity }}
        <button @click="removeFromCart(item.id)">移除</button>
      </li>
    </ul>
    <p>购物车总价:{{ cartTotalPrice }}</p>
  </div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
export default {
  computed: {
   ...mapGetters(['cartItems', 'cartTotalPrice'])
  },
  methods: {
   ...mapActions(['removeItemFromCart'])
  }
};
</script>

通过 Vuex,不同层级、不同功能的组件能够统一地获取和修改共享的状态,使得整个应用的数据管理更加规范、高效,尤其适合多人协作开发的大型项目。

四、组件通信案例实战

为了更深入地理解 Vue 组件通信在实际项目中的应用,我们不妨构建一个简单的记事本应用。在这个应用中,包含了父子组件以及兄弟组件之间的交互,通过巧妙运用前面所介绍的通信方式,来实现一个功能完备的小项目。

首先,对记事本应用进行组件拆分:

  • TodoHeader:负责输入新任务,包含一个输入框和添加按钮,用户在此输入待办事项并点击添加。
  • TodoBody:展示任务列表,每个任务项带有删除按钮,用于移除已完成或不再需要的任务。
  • TodoFooter:显示任务总数,并提供清空所有任务的功能按钮。

在 App.vue 根组件中,引入并组合这三个组件:

xml 复制代码
<template>
  <div id="app">
    <TodoHeader @addTask="addTask" />
    <TodoBody :list="list" @delTask="delTask" />
    <TodoFooter :list="list" @clearAll="clearAll" />
  </div>
</template>
<script>
import TodoHeader from './components/TodoHeader.vue';
import TodoBody from './components/TodoBody.vue';
import TodoFooter from './components/TodoFooter.vue';
export default {
  data() {
    return {
      list: []
    };
  },
  methods: {
    addTask(task) {
      this.list.unshift({ id: Date.now(), name: task });
    },
    delTask(id) {
      this.list = this.list.filter(item => item.id!== id);
    },
    clearAll() {
      this.list = [];
    }
  },
  components: {
    TodoHeader,
    TodoBody,
    TodoFooter
  }
};
</script>

在 TodoHeader 组件中,通过 v-model 双向绑定输入框的值,当用户按下回车键或点击添加按钮时,使用 $emit 向父组件 App.vue 发送 addTask 事件,并传递新任务的名称:

xml 复制代码
<template>
  <header class="header">
    <h1>记事本</h1>
    <input placeholder="请输入任务" class="new-todo" v-model="inputTask" @keyup.enter="addTask" />
    <button class="add" @click="addTask()">添加任务</button>
  </header>
</template>
<script>
export default {
  data() {
    return {
      inputTask: ""
    };
  },
  methods: {
    addTask() {
      if (this.inputTask.trim() === "") {
        alert("请输入任务");
        return;
      }
      this.$emit('addTask', this.inputTask);
      this.inputTask = "";
    }
  }
};
</script>

TodoBody 组件接收父组件 App.vue 通过 props 传递的任务列表 list,并使用 v-for 指令遍历展示每个任务项。每个任务项的删除按钮绑定点击事件,通过 $emit 触发 delTask 事件,将当前任务的 id 传递回父组件,以便父组件执行删除操作:

xml 复制代码
<template>
  <section class="main">
    <ul class="todo-list" v-for="(item, index) in list" :key="item.id">
      <li class="todo">
        <div class="view">
          <span class="index">{{ index + 1 }}.</span>
          <label>{{ item.name }}</label>
          <button class="destroy" @click="delTask(item.id)">×</button>
        </div>
      </li>
    </ul>
  </section>
</template>
<script>
export default {
  props: {
    list: Array
  },
  methods: {
    delTask(id) {
      this.$emit('delTask', id);
    }
  }
};
</script>

TodoFooter 组件同样接收父组件传递的任务列表 list,用于展示任务总数。当点击清空按钮时,触发 clearAll 事件通知父组件清空所有任务:

xml 复制代码
<template>
  <footer class="footer">
    <span class="todo-count">合计: <strong>{{ list.length }}</strong></span>
    <button class="clear-completed" @click="clearAll()">清空任务</button>
  </footer>
</template>
<script>
export default {
  props: {
    list: Array
  },
  methods: {
    clearAll() {
      this.$emit('clearAll');
    }
  }
};
</script>

在这个记事本应用案例中,父子组件之间主要通过 props 传递数据和 $emit 触发事件进行通信,使得数据的流向清晰明了,父组件能够掌控子组件的关键操作并及时更新状态。各个组件各司其职,又紧密协作,充分展现了 Vue 组件通信的精妙之处,为构建复杂而有序的应用奠定了坚实基础。

五、总结

bash 复制代码
Vue 组件通信方式丰富多样,每种方式都有其独特的适用场景。父子组件通信中,props、$emit、v-model、ref等各显神通,从数据传递到方法调用,满足了不同层次的交互需求;非父子组件通信里,EventBus、provide /inject、Vuex 则突破层级限制,为兄弟组件、跨层级组件间的数据共享与交互提供了解决方案。在实际项目开发中,我们需要依据项目的规模、组件结构的复杂程度以及数据流向的特点,审慎地选择合适的通信方式。只有这样,才能构建出结构清晰、易于维护的 Vue 应用。希望各位读者能将这些知识运用到实践中,不断探索,挖掘出更多高效的组件通信技巧,让 Vue 项目开发变得更加得心应手。
相关推荐
星陈~3 小时前
检测electron打包文件 app.asar
前端·vue.js·electron
仿生狮子3 小时前
CSS Layer、Tailwind 和 sass 如何共存?
javascript·css·vue.js
在路上`3 小时前
vue3使用AntV X6 (图可视化引擎)历程[二]
javascript·vue.js
小盼江5 小时前
智能服装推荐系统 协同过滤余弦函数推荐服装 Springboot Vue Element-UI前后端分离
大数据·数据库·vue.js·spring boot·ui·毕业设计
CodeChampion5 小时前
69.基于SpringBoot + Vue实现的前后端分离-家乡特色推荐系统(项目 + 论文PPT)
java·vue.js·spring boot·mysql·elementui·node.js·mybatis
豆豆(设计前端)6 小时前
总结 Vue 请求接口的各种类型及传参方式
前端·javascript·vue.js
BestArsenaI6 小时前
vue -关于浏览器localstorge数据定期清除的实现
javascript·vue.js·ecmascript
Smile_Gently6 小时前
Element-plus、Element-ui之Tree 树形控件回显Bug问题。
javascript·vue.js·elementui
城沐小巷6 小时前
基于SpringBoot+Vue助农管理系统的设计与实现
vue.js·spring boot·助农管理系统
武昌库里写JAVA7 小时前
Redis 笔记(二)-Redis 安装及测试
数据结构·vue.js·spring boot·算法·课程设计