整体学习路线:简易弹窗 → 完善基础功能 → 组件内部状态管理 → 父→子传值 → 子→父传值 → 跨组件传值(最终目标)
步骤 1:搭建最基础的弹窗(静态结构,无交互)
目标:实现一个固定显示在页面中的弹窗,包含标题、内容、关闭按钮,掌握 Vue 组件的基本结构。
组件文件:BasicPopup.vue
1<template> 2 <!-- 弹窗外层容器(遮罩层) --> 3 <div class="popup-mask"> 4 <!-- 弹窗主体 --> 5 <div class="popup-content"> 6 <h3>简易弹窗</h3> 7 <p>这是最基础的弹窗内容</p> 8 <button>关闭</button> 9 </div> 10 </div> 11</template> 12 13<script setup> 14// 现阶段无逻辑,仅搭建结构 15</script> 16 17<style scoped> 18/* 遮罩层:占满整个屏幕,半透明背景 */ 19.popup-mask { 20 position: fixed; 21 top: 0; 22 left: 0; 23 right: 0; 24 bottom: 0; 25 background-color: rgba(0, 0, 0, 0.5); 26 display: flex; 27 justify-content: center; 28 align-items: center; 29} 30 31/* 弹窗主体:白色背景,固定宽高,圆角 */ 32.popup-content { 33 width: 400px; 34 padding: 20px; 35 background-color: #fff; 36 border-radius: 8px; 37 text-align: center; 38} 39 40/* 按钮样式 */ 41button { 42 margin-top: 20px; 43 padding: 8px 16px; 44 background-color: #1890ff; 45 color: #fff; 46 border: none; 47 border-radius: 4px; 48 cursor: pointer; 49} 50</style> 51
使用组件(App.vue)
1<template> 2 <h1>弹窗学习演示</h1> 3 <BasicPopup /> 4</template> 5 6<script setup> 7import BasicPopup from './components/BasicPopup.vue'; 8</script> 9
步骤 2:添加基础交互(控制弹窗显示/隐藏)
目标:通过「响应式状态」控制弹窗的显示与隐藏,给关闭按钮添加点击事件,掌握 ref 和事件绑定。
改造 BasicPopup.vue(新增响应式状态和点击事件)
1<template> 2 <!-- 用 v-if 控制弹窗是否显示 --> 3 <div class="popup-mask" v-if="isShow"> 4 <div class="popup-content"> 5 <h3>简易弹窗</h3> 6 <p>这是最基础的弹窗内容</p> 7 <!-- 绑定关闭按钮点击事件 --> 8 <button @click="closePopup">关闭</button> 9 </div> 10 </div> 11</template> 12 13<script setup> 14// 1. 导入 ref 用于创建响应式状态 15import { ref } from 'vue'; 16 17// 2. 定义响应式变量,控制弹窗显示/隐藏 18const isShow = ref(true); // 初始值为 true,默认显示弹窗 19 20// 3. 定义关闭弹窗的方法 21const closePopup = () => { 22 isShow.value = false; // 响应式变量修改需要通过 .value 23}; 24</script> 25 26<style scoped> 27/* 样式同步骤 1,不变 */ 28.popup-mask { 29 position: fixed; 30 top: 0; 31 left: 0; 32 right: 0; 33 bottom: 0; 34 background-color: rgba(0, 0, 0, 0.5); 35 display: flex; 36 justify-content: center; 37 align-items: center; 38} 39 40.popup-content { 41 width: 400px; 42 padding: 20px; 43 background-color: #fff; 44 border-radius: 8px; 45 text-align: center; 46} 47 48button { 49 margin-top: 20px; 50 padding: 8px 16px; 51 background-color: #1890ff; 52 color: #fff; 53 border: none; 54 border-radius: 4px; 55 cursor: pointer; 56} 57</style> 58
补充:在 App.vue 添加「打开弹窗」按钮
我们知道,Vue 遵循「单向数据流」和「组件封装隔离」,子组件内部的方法 / 私有状态默认是对外隐藏的,外部父组件无法直接访问。
而 ref 就是打破这种 “隔离” 的合法方式(非侵入式),让父组件能够:
- 调用子组件通过
defineExpose暴露的方法(如openPopup方法,用于打开弹窗)。 - 访问子组件通过
defineExpose暴露的响应式状态(如弹窗内部的isShow状态)。 - 实现「父组件主动控制子组件」的交互场景(如主动打开 / 关闭弹窗、主动刷新子组件数据)。
1<template> 2 <h1>弹窗学习演示</h1> 3 <!-- 新增打开弹窗按钮 --> 4 <button @click="handleOpenPopup">打开弹窗</button> 5 <BasicPopup ref="popupRef" /> 6</template> 7 8<script setup> 9import { ref } from 'vue'; 10import BasicPopup from './components/BasicPopup.vue'; 11 12// 获取弹窗组件实例 13const popupRef = ref(null); 14 15// 打开弹窗的方法(调用子组件的方法,后续步骤会完善) 16const handleOpenPopup = () => { 17 if (popupRef.value) { 18 popupRef.value.openPopup(); 19 } 20}; 21</script> 22 23<style> 24button { 25 margin: 10px; 26 padding: 8px 16px; 27 background-color: #1890ff; 28 color: #fff; 29 border: none; 30 border-radius: 4px; 31 cursor: pointer; 32} 33</style> 34
给 BasicPopup.vue 补充「打开弹窗」方法(暴露给父组件)
1<template> 2 <div class="popup-mask" v-if="isShow"> 3 <div class="popup-content"> 4 <h3>简易弹窗</h3> 5 <p>这是最基础的弹窗内容</p> 6 <button @click="closePopup">关闭</button> 7 </div> 8 </div> 9</template> 10 11<script setup> 12import { ref } from 'vue'; 13 14const isShow = ref(false); // 初始值改为 false,默认隐藏 15 16const closePopup = () => { 17 isShow.value = false; 18}; 19 20// 新增:打开弹窗的方法 21const openPopup = () => { 22 isShow.value = true; 23}; 24 25// 暴露组件方法,让父组件可以调用(关键) 26defineExpose({ 27 openPopup 28}); 29</script> 30 31<style scoped> 32/* 样式同前 */ 33</style> 34
核心知识点
ref创建响应式状态,修改时需要.value@click事件绑定,触发自定义方法defineExpose暴露组件内部方法/状态,供父组件调用v-if控制元素的渲染与销毁(实现弹窗显示/隐藏)
步骤 3:父组件 → 子组件传值(Props 传递)
目标:父组件向弹窗组件传递「弹窗标题」和「弹窗内容」,掌握 defineProps 的使用,实现弹窗内容的动态化。
改造 BasicPopup.vue(接收父组件传递的参数)
1<template> 2 <div class="popup-mask" v-if="isShow"> 3 <div class="popup-content"> 4 <!-- 渲染父组件传递的标题 --> 5 <h3>{{ popupTitle }}</h3> 6 <!-- 渲染父组件传递的内容 --> 7 <p>{{ popupContent }}</p> 8 <button @click="closePopup">关闭</button> 9 </div> 10 </div> 11</template> 12 13<script setup> 14import { ref } from 'vue'; 15 16// 1. 定义 Props,接收父组件传递的值(指定类型和默认值) 17const props = defineProps({ 18 // 弹窗标题 19 popupTitle: { 20 type: String, 21 default: '默认弹窗标题' // 默认值,防止父组件未传递 22 }, 23 // 弹窗内容 24 popupContent: { 25 type: String, 26 default: '默认弹窗内容' 27 } 28}); 29 30const isShow = ref(false); 31 32const closePopup = () => { 33 isShow.value = false; 34}; 35 36const openPopup = () => { 37 isShow.value = true; 38}; 39 40defineExpose({ 41 openPopup 42}); 43</script> 44 45<style scoped> 46/* 样式同前 */ 47</style> 48
改造 App.vue(向子组件传递 Props 数据)
1<template> 2 <h1>弹窗学习演示</h1> 3 <button @click="openPopup">打开弹窗</button> 4 <!-- 向子组件传递 props 数据(静态传递 + 动态传递均可) --> 5 <BasicPopup 6 ref="popupRef" 7 popupTitle="父组件传递的标题" 8 :popupContent="dynamicContent" 9 /> 10</template> 11 12<script setup> 13import { ref } from 'vue'; 14import BasicPopup from './components/BasicPopup.vue'; 15 16const popupRef = ref(null); 17// 动态定义弹窗内容(也可以是静态字符串) 18const dynamicContent = ref('这是父组件通过 Props 传递的动态弹窗内容~'); 19 20const openPopup = () => { 21 if (popupRef.value) { 22 popupRef.value.openPopup(); 23 } 24}; 25</script> 26 27<style> 28button { 29 margin: 10px; 30 padding: 8px 16px; 31 background-color: #1890ff; 32 color: #fff; 33 border: none; 34 border-radius: 4px; 35 cursor: pointer; 36} 37</style> 38
核心知识点
defineProps定义组件接收的参数,支持类型校验和默认值- Props 传递规则:父组件通过「属性绑定」向子组件传值,子组件只读(不能修改 Props,遵循单向数据流)
- 静态传值(直接写字符串)直接把等号后面的内容作为纯字符串传递给子组件的 Props,Vue 不会对其做任何解析、计算,原样传递。动态传值(
:xxx="变量") Vue 会先解析求值,再把结果传递给子组件的 Props。
步骤 4:子组件 → 父组件传值(Emits 事件派发)
目标:弹窗关闭时,向父组件传递「弹窗关闭的状态」和「自定义数据」,掌握 defineEmits 的使用,实现子向父的通信。
改造 BasicPopup.vue(派发事件给父组件)
1<template> 2 <div class="popup-mask" v-if="isShow"> 3 <div class="popup-content"> 4 <h3>{{ popupTitle }}</h3> 5 <p>{{ popupContent }}</p> 6 <!-- 关闭按钮点击时,触发事件派发 --> 7 <button @click="handleClose">关闭</button> 8 </div> 9 </div> 10</template> 11 12<script setup> 13import { ref } from 'vue'; 14 15// 1. 定义 Props 16const props = defineProps({ 17 popupTitle: { 18 type: String, 19 default: '默认弹窗标题' 20 }, 21 popupContent: { 22 type: String, 23 default: '默认弹窗内容' 24 } 25}); 26 27// 2. 定义 Emits,声明要派发的事件(支持数组/对象格式,对象格式可校验) 28const emit = defineEmits([ 29 'popup-close', // 弹窗关闭事件 30 'send-data' // 向父组件传递数据的事件 31]); 32 33const isShow = ref(false); 34 35// 3. 改造关闭方法,派发事件给父组件 36const handleClose = () => { 37 isShow.value = false; 38 39 // 派发「popup-close」事件,可携带参数(可选) 40 emit('popup-close', { 41 closeTime: new Date().toLocaleString(), 42 message: '弹窗已正常关闭' 43 }); 44 45 // 派发「send-data」事件,传递自定义数据 46 emit('send-data', '这是子组件向父组件传递的额外数据'); 47}; 48 49const openPopup = () => { 50 isShow.value = true; 51}; 52 53defineExpose({ 54 openPopup 55}); 56</script> 57 58<style scoped> 59/* 样式同前 */ 60</style> 61
改造 App.vue(监听子组件派发的事件)
1<template> 2 <h1>弹窗学习演示</h1> 3 <button @click="openPopup">打开弹窗</button> 4 <!-- 监听子组件派发的事件,绑定处理方法 --> 5 <BasicPopup 6 ref="popupRef" 7 popupTitle="父组件传递的标题" 8 :popupContent="dynamicContent" 9 @popup-close="handlePopupClose" 10 @send-data="handleReceiveData" 11 /> 12</template> 13 14<script setup> 15import { ref } from 'vue'; 16import BasicPopup from './components/BasicPopup.vue'; 17 18const popupRef = ref(null); 19const dynamicContent = ref('这是父组件通过 Props 传递的动态弹窗内容~'); 20 21const openPopup = () => { 22 if (popupRef.value) { 23 popupRef.value.openPopup(); 24 } 25}; 26 27// 1. 处理子组件派发的「popup-close」事件 28const handlePopupClose = (closeInfo) => { 29 console.log('接收弹窗关闭信息:', closeInfo); 30 alert(`弹窗已关闭,关闭时间:${closeInfo.closeTime}`); 31}; 32 33// 2. 处理子组件派发的「send-data」事件 34const handleReceiveData = (data) => { 35 console.log('接收子组件传递的数据:', data); 36 alert([`收到子组件数据:${data}`](https://xplanc.org/primers/document/zh/03.HTML/EX.HTML%20%E5%85%83%E7%B4%A0/EX.data.md)); 37}; 38</script> 39 40<style> 41button { 42 margin: 10px; 43 padding: 8px 16px; 44 background-color: #1890ff; 45 color: #fff; 46 border: none; 47 border-radius: 4px; 48 cursor: pointer; 49} 50</style> 51
核心知识点
defineEmits声明组件要派发的事件,遵循「事件名小写+短横线分隔」规范emit方法用于派发事件,第一个参数是事件名,后续参数是要传递的数据- 父组件通过
@事件名监听子组件事件,处理方法的参数就是子组件传递的数据 - 单向数据流补充:子组件不能直接修改 Props,如需修改,可通过「子组件派发事件 → 父组件修改数据 → Props 重新传递」实现
步骤 5:跨组件传值(非父子组件,使用 Provide / Inject)
目标:实现「非父子组件」(如:Grandpa.vue → Parent.vue → Popup.vue,爷爷组件向弹窗组件传值)的通信,掌握 provide / inject 的使用,这是 Vue 中跨组件传值的核心方案之一。
步骤 5.1:创建层级组件结构
1├── App.vue(入口) 2├── components/ 3│ ├── Grandpa.vue(爷爷组件,提供数据) 4│ ├── Parent.vue(父组件,中间层级,无数据处理) 5│ └── Popup.vue(弹窗组件,注入并使用数据) 6
步骤 5.2:爷爷组件 Grandpa.vue(提供数据,provide)
1<template> 2 <div class="grandpa"> 3 <h2>爷爷组件</h2> 4 <button @click="updateGlobalData">修改跨组件传递的数据</button> 5 <!-- 引入父组件(中间层级) --> 6 <Parent /> 7 </div> 8</template> 9 10<script setup> 11import { ref, provide } from 'vue'; 12import Parent from './Parent.vue'; 13 14// 1. 定义要跨组件传递的响应式数据 15const globalPopupConfig = ref({ 16 author: 'yyt', 17 version: '1.0.0', 18 theme: 'light', 19 maxWidth: '500px' 20}); 21 22// 2. 提供(provide)数据,供后代组件注入使用 23// 第一个参数:注入标识(字符串/Symbol),第二个参数:要传递的数据 24provide('popupGlobalConfig', globalPopupConfig); 25 26// 3. 修改响应式数据(后代组件会同步更新) 27const updateGlobalData = () => { 28 globalPopupConfig.value = { 29 ...globalPopupConfig.value, 30 version: '2.0.0', 31 theme: 'dark', 32 updateTime: new Date().toLocaleString() 33 }; 34}; 35</script> 36 37<style scoped> 38.grandpa { 39 padding: 20px; 40 border: 2px solid #666; 41 border-radius: 8px; 42 margin: 10px; 43} 44 45button { 46 padding: 8px 16px; 47 background-color: #1890ff; 48 color: #fff; 49 border: none; 50 border-radius: 4px; 51 cursor: pointer; 52} 53</style> 54
步骤 5.3:父组件 Parent.vue(中间层级,仅做组件嵌套)
1<template> 2 <div class="parent"> 3 <h3>父组件(中间层级)</h3> 4 <button @click="openPopup">打开跨组件传值的弹窗</button> 5 <!-- 引入弹窗组件 --> 6 <Popup ref="popupRef" /> 7 </div> 8</template> 9 10<script setup> 11import { ref } from 'vue'; 12import Popup from './Popup.vue'; 13 14const popupRef = ref(null); 15 16// 打开弹窗 17const openPopup = () => { 18 if (popupRef.value) { 19 popupRef.value.openPopup(); 20 } 21}; 22</script> 23 24<style scoped> 25.parent { 26 padding: 20px; 27 border: 2px solid #999; 28 border-radius: 8px; 29 margin: 10px; 30 margin-top: 20px; 31} 32 33button { 34 padding: 8px 16px; 35 background-color: #1890ff; 36 color: #fff; 37 border: none; 38 border-radius: 4px; 39 cursor: pointer; 40} 41</style> 42
步骤 5.4:弹窗组件 Popup.vue(注入数据,inject)
1<template> 2 <div class="popup-mask" v-if="isShow" :style="{ backgroundColor: themeBg }"> 3 <div class="popup-content" :style="{ maxWidth: globalConfig.maxWidth, background: globalConfig.theme === 'dark' ? '#333' : '#fff', color: globalConfig.theme === 'dark' ? '#fff' : '#333' }"> 4 <h3>{{ popupTitle }}</h3> 5 <p>{{ popupContent }}</p> 6 <!-- 渲染跨组件传递的数据 --> 7 <div class="global-info" style="margin: 15px 0; padding: 10px; border: 1px solid #eee; border-radius: 4px;"> 8 <p>跨组件传递的配置:</p> 9 <p>作者:{{ globalConfig.author }}</p> 10 <p>版本:{{ globalConfig.version }}</p> 11 <p>主题:{{ globalConfig.theme }}</p> 12 <p v-if="globalConfig.updateTime">更新时间:{{ globalConfig.updateTime }}</p> 13 </div> 14 <button @click="handleClose">关闭</button> 15 </div> 16 </div> 17</template> 18 19<script setup> 20import { ref, inject, computed } from 'vue'; 21 22// 1. 定义 Props 23const props = defineProps({ 24 popupTitle: { 25 type: String, 26 default: '默认弹窗标题' 27 }, 28 popupContent: { 29 type: String, 30 default: '默认弹窗内容' 31 } 32}); 33 34// 2. 注入(inject)爷爷组件提供的数据 35// 第一个参数:注入标识(与 provide 一致),第二个参数:默认值(可选) 36const globalConfig = inject('popupGlobalConfig', ref({ author: '默认作者', version: '0.0.1' })); 37 38// 3. 基于注入的数据创建计算属性(可选,优化使用体验) 39const themeBg = computed(() => { 40 return globalConfig.value.theme === 'dark' ? 'rgba(0, 0, 0, 0.8)' : 'rgba(0, 0, 0, 0.5)'; 41}); 42 43// 4. 定义 Emits 44const emit = defineEmits(['popup-close']); 45 46const isShow = ref(false); 47 48// 5. 关闭方法 49const handleClose = () => { 50 isShow.value = false; 51 emit('popup-close', { message: '弹窗已关闭' }); 52}; 53 54// 6. 打开弹窗方法 55const openPopup = () => { 56 isShow.value = true; 57}; 58 59// 7. 暴露方法 60defineExpose({ 61 openPopup 62}); 63</script> 64 65<style scoped> 66.popup-mask { 67 position: fixed; 68 top: 0; 69 left: 0; 70 right: 0; 71 bottom: 0; 72 display: flex; 73 justify-content: center; 74 align-items: center; 75} 76 77.popup-content { 78 padding: 20px; 79 border-radius: 8px; 80 text-align: center; 81} 82 83button { 84 margin-top: 20px; 85 padding: 8px 16px; 86 background-color: #1890ff; 87 color: #fff; 88 border: none; 89 border-radius: 4px; 90 cursor: pointer; 91} 92</style> 93
步骤 5.5:入口 App.vue(引入爷爷组件)
1<template> 2 <h1>跨组件传值演示(Provide / Inject)</h1> 3 <Grandpa /> 4</template> 5 6<script setup> 7import Grandpa from './components/Grandpa.vue'; 8</script> 9
核心知识点
provide/inject用于跨层级组件通信(无论层级多深),爷爷组件提供数据,后代组件注入使用- 传递响应式数据:
provide时传递ref/reactive包装的数据,后代组件可感知数据变化(同步更新) - 注入标识:建议使用
Symbol避免命名冲突(生产环境推荐),本步骤为简化使用字符串标识 - 注入默认值:
inject第二个参数为默认值,防止祖先组件未提供数据时出现报错 - 与 Props/Emits 的区别:Props 适用于父子组件,
provide/inject适用于跨层级组件
《从零构建 Vue 弹窗组件》 是转载文章,点击查看原文。