Vue组件通信保姆级案例教程每个方法一个例子看完直接上手用之前我们聊了Vue的基础、路由和状态管理。今天要解决的是你一旦开始用组件写页面就一定会碰上的问题组件之间怎么传数据、怎么互相通知很多新手在这里卡住因为Vue提供了好几种通信方式不知道该用哪个。今天我就用一个一个独立的小案例把每种方式讲清楚。每个案例都是一个完整的小场景代码全部有注释你照着抄一遍就能跑起来。案例一父传子 Props——展示用户卡片场景父组件里有一份用户信息姓名、年龄子组件负责把这张卡片画出来。1. 父组件 App.vuevuetemplate div classapp h1父组件/h1 !-- 使用子组件并传递两个属性userName 和 userAge -- !-- 注意userAge 前面有冒号表示传的是数字 25不加冒号会被当成字符串 25 -- UserCard :user-namename :user-age25 / /div /template script setup // 引入子组件 import UserCard from ./components/UserCard.vue // ref 用来创建一个响应式数据 import { ref } from vue // 父组件自己的数据姓名 const name ref(小明) /script2. 子组件 UserCard.vuevuetemplate div classuser-card h3用户卡片/h3 !-- 在模板中直接使用 props 里声明的变量名 -- p姓名{{ userName }}/p p年龄{{ userAge }}/p /div /template script setup // defineProps 是一个内置的宏用来声明子组件可以接收哪些属性 // 属性名从父组件传过来的 kebab-case (短横线) 会自动转成 camelCase (驼峰) const props defineProps({ userName: { type: String, // 类型校验必须是字符串 required: true // 这个属性必须传否则控制台会警告 }, userAge: { type: Number, // 类型校验必须是数字 default: 18 // 如果父组件没传就用默认值 18 } }) // 在 JavaScript 里使用 props 时用 props.xxx console.log(接收到的姓名, props.userName) console.log(接收到的年龄, props.userAge) /script案例小结Props 是单向的数据只能从父组件流向子组件。子组件不能直接修改props 的值要改只能让父组件改。案例二子传父 Emit——点赞按钮场景子组件是一个点赞按钮用户点一下父组件统计的总点赞数加一。1. 父组件 App.vuevuetemplate div h2当前总点赞数{{ likeCount }}/h2 !-- 监听子组件发出的 like 事件一旦触发就调用 handleLike 方法 -- LikeButton likehandleLike / /div /template script setup import { ref } from vue import LikeButton from ./components/LikeButton.vue // 点赞数初始化为 0 const likeCount ref(0) // 处理点赞事件的方法 function handleLike() { likeCount.value // 点赞数加 1 } /script2. 子组件 LikeButton.vuevuetemplate div !-- 点击按钮时调用 onLike 函数 -- button clickonLike❤️ 点赞/button /div /template script setup // defineEmits 用来声明这个组件可以发出哪些事件返回一个 emit 函数 const emit defineEmits([like]) // 点击后触发 like 事件父组件就能收到 function onLike() { // 这里还可以传参数比如 emit(like, 参数内容) emit(like) } /script案例小结子传父的核心就是 emit。子组件声明事件并触发父组件在模板里用事件名监听。案例三v-model 双向绑定——自定义输入框场景封装一个输入框组件父组件用 v-model 就能直接拿到输入的内容。1. 父组件 App.vuevuetemplate div !-- v-model 绑定到 inputText跟原生的 input 一样好用 -- MyInput v-modelinputText / p你输入的内容{{ inputText }}/p /div /template script setup import { ref } from vue import MyInput from ./components/MyInput.vue // 用来存放输入框的内容 const inputText ref() /script2. 子组件 MyInput.vuevuetemplate div !-- :valuemodelValue 将接收到的值显示在输入框中 input 事件触发时执行更新操作 -- input :valuemodelValue input$emit(update:modelValue, $event.target.value) placeholder请输入内容 / /div /template script setup // v-model 默认传过来的 prop 名字必须是 modelValue defineProps({ modelValue: { type: String, default: } }) // v-model 默认监听的事件名必须是 update:modelValue defineEmits([update:modelValue]) /script案例小结v-model 本质上是:modelValueupdate:modelValue的组合。你也可以用v-model:xxx传多个值原理一样。案例四兄弟组件通信——搜索框与列表联动场景一个搜索框组件一个列表组件。在搜索框输入关键词列表组件根据关键词筛选显示内容。它们没有父子关系是兄弟。解决方法借助它们共同的父组件作为“中转站”。1. 父组件 App.vue中转站vuetemplate div !-- 搜索框组件输入时发出 search 事件携带输入的内容 -- SearchBox searchhandleSearch / !-- 列表组件接收 filterKeyword 作为筛选条件 -- ContentList :keywordfilterKeyword / /div /template script setup import { ref } from vue import SearchBox from ./components/SearchBox.vue import ContentList from ./components/ContentList.vue // 父组件维护一个关键词 const filterKeyword ref() // 当搜索框发出 search 事件时更新关键词 function handleSearch(keyword) { filterKeyword.value keyword } /script2. 搜索框组件 SearchBox.vuevuetemplate div input :valuekeyword inputonInput placeholder请输入搜索词 / /div /template script setup import { ref } from vue const keyword ref() const emit defineEmits([search]) function onInput(e) { keyword.value e.target.value // 每次输入内容变化就发出 search 事件把内容传给父组件 emit(search, keyword.value) } /script3. 列表组件 ContentList.vuevuetemplate div ul !-- 遍历筛选后的列表并显示 -- li v-foritem in filteredList :keyitem{{ item }}/li /ul /div /template script setup import { computed } from vue // 接收父组件传来的 keyword const props defineProps({ keyword: { type: String, default: } }) // 假数据 const allItems [苹果, 香蕉, 橘子, 葡萄, 西瓜] // 计算属性根据 keyword 筛选数据 const filteredList computed(() { // 如果没输入关键词就显示全部 if (!props.keyword) return allItems // 否则返回名字里包含关键词的项 return allItems.filter(item item.includes(props.keyword)) }) /script案例小结兄弟组件通信说白了就是把一个组件的 emit 和另一个组件的 props 结合起来通过父组件中转一下。案例五跨级通信 provide/inject——一键切换主题场景顶层组件提供一个“主题色”所有子孙组件都能直接拿到不用一层层通过 props 往下传。1. 祖先组件 App.vuevuetemplate div h2主题{{ theme }}/h2 button clicktoggleTheme切换主题/button ChildLevel / /div /template script setup import { ref, provide } from vue import ChildLevel from ./components/ChildLevel.vue const theme ref(浅色) // 使用 provide 提供数据第一个参数是 key字符串第二个是值 provide(appTheme, theme) function toggleTheme() { theme.value theme.value 浅色 ? 深色 : 浅色 } /script2. 中间层组件 ChildLevel.vuevuetemplate div h3中间层组件/h3 GrandChild / /div /template script setup import GrandChild from ./GrandChild.vue /script3. 孙子组件 GrandChild.vuevuetemplate div p孙子组件拿到的主题{{ theme }}/p /div /template script setup import { inject } from vue // 使用 inject 获取祖先提供的 appTheme 数据 const theme inject(appTheme) /script案例小结provide/inject 很适合全局配置、主题、语言这类深层传递的数据。但不要滥用因为它会让数据流向不那么清晰。案例六父调子方法 defineExpose——倒计时控制场景子组件内部有一个倒计时功能。父组件想直接调用子组件的“开始”和“重置”方法。1. 子组件 Countdown.vuevuetemplate div p倒计时{{ count }}/p /div /template script setup import { ref } from vue const count ref(10) // 倒计时数字 let timer null // 定时器 ID function start() { // 如果已经有定时器在跑先清除 if (timer) clearInterval(timer) timer setInterval(() { if (count.value 0) { count.value-- } else { clearInterval(timer) // 到 0 停止 } }, 1000) } function reset() { // 清除定时器重置数字 clearInterval(timer) count.value 10 } // 把想要暴露给父组件调用的东西列出来 defineExpose({ start, reset }) /script2. 父组件 App.vuevuetemplate div !-- 给子组件加个 ref这样就能拿到子组件实例 -- Countdown refcountdownRef / button clickstartCountdown开始/button button clickresetCountdown重置/button /div /template script setup import { ref } from vue import Countdown from ./components/Countdown.vue // 这个 ref 的名字要和模板里 refcountdownRef 匹配 const countdownRef ref(null) function startCountdown() { // 通过 .value 拿到子组件实例然后调用它暴露出来的方法 countdownRef.value.start() } function resetCountdown() { countdownRef.value.reset() } /script案例小结defineExposeref可以让父组件直接操作子组件但不要大面积使用否则组件耦合太紧。案例七全局状态管理 Pinia——购物车场景多个页面、多个组件都需要读写购物车数据。这时候用 Pinia 最合适。1. 安装 Pinia如果还没装bashnpm install pinia在 main.js 中注册javascriptimport { createPinia } from pinia const app createApp(App) app.use(createPinia()) app.mount(#app)2. 定义 storestores/cart.jsjavascriptimport { defineStore } from pinia import { ref, computed } from vue export const useCartStore defineStore(cart, () { // state购物车商品列表每项 { id, name, price, count } const items ref([]) // getter总价 const totalPrice computed(() { return items.value.reduce((sum, item) sum item.price * item.count, 0) }) // action添加商品 function addItem(product) { const exist items.value.find(item item.id product.id) if (exist) { exist.count } else { items.value.push({ ...product, count: 1 }) } } // action清空购物车 function clearCart() { items.value [] } return { items, totalPrice, addItem, clearCart } })3. 页面组件中使用vuetemplate div h2购物车总价{{ cart.totalPrice }} 元/h2 ul li v-foritem in cart.items :keyitem.id {{ item.name }} - {{ item.price }} 元 × {{ item.count }} /li /ul button clickaddSample添加一个苹果/button button clickcart.clearCart清空/button /div /template script setup import { useCartStore } from /stores/cart const cart useCartStore() function addSample() { // 添加示例商品 cart.addItem({ id: 1, name: 苹果, price: 5 }) } /script案例小结Pinia 把共享的数据和方法集中管理任何组件直接引入就能用是解决复杂通信的终极方案。练习题选择题父组件向子组件传递数据应该使用 A. emitB. propsC. provideD. v-model子组件通知父组件“数据已变化”应该使用 A. 直接修改 propsB. defineExposeC. emitD. providev-model 在组件上默认绑定的 prop 名称是 A. valueB. modelValueC. vModelD. inputValue判断题子组件可以直接修改父组件传来的 props 的值。 provide/inject 适用于任意层级的组件通信且数据流向容易追踪。 编程题实现一个“收藏”功能父组件显示收藏状态已收藏/未收藏子组件是一个按钮点击切换收藏状态要求使用 props 和 emit使用 Pinia 实现一个笔记便签store 里维护一个 notes 数组每个 note 包含 id 和 text一个组件负责添加新便签另一个组件负责展示便签列表答案BCB错误props 是只读的。错误provide/inject 的数据流向较隐晦不便于追踪调试。实现参考父组件vuetemplate div p状态{{ isCollected ? 已收藏 : 未收藏 }}/p CollectButton :collectedisCollected toggleisCollected !isCollected / /div /template script setup import { ref } from vue import CollectButton from ./CollectButton.vue const isCollected ref(false) /script子组件 CollectButton.vuevuetemplate button click$emit(toggle) {{ collected ? ❤️ 取消收藏 : 收藏 }} /button /template script setup defineProps({ collected: Boolean }) defineEmits([toggle]) /script参考stores/notes.jsjavascriptimport { defineStore } from pinia import { ref } from vue export const useNotesStore defineStore(notes, () { const notes ref([]) let nextId 1 function addNote(text) { notes.value.push({ id: nextId, text }) } return { notes, addNote } })添加组件vuetemplate input v-modeltext placeholder输入便签 / button clicknotes.addNote(text); text添加/button /template script setup import { ref } from vue import { useNotesStore } from /stores/notes const notes useNotesStore() const text ref() /script列表组件vuetemplate ulli v-forn in notes.notes :keyn.id{{ n.text }}/li/ul /template script setup import { useNotesStore } from /stores/notes const notes useNotesStore() /script最后唠叨两句组件通信是Vue里最有“工程感”的知识点初学容易晕但只要把上面的案例一个个敲一遍你就能摸清每种方式的脾气。遇到实际问题先想数据是谁的谁要听谁的话再按上面这些套路去选方案十有八九都能搞定。有问题评论区直接问看到必回。下篇见。