很多初学者学Vue,前四天还跟得上节奏,一到第五天就有点懵——因为从这一天开始,你不再只是"写页面",而是开始写"组件"了。我自己的感受是:模板语法、指令、状态管理这些学起来都还算直观,真正让脑子转个弯的,恰恰是组件化这一关。Day5这个节点,就是把"页面思维"切换到"组件思维"的关键一天。这篇文章就围绕第五天的核心内容展开,把组件化这条主线讲透,配合能直接跑的代码示例和踩坑记录,帮还在组件门口徘徊的朋友顺利迈过去。1. 组件化到底改变了什么1.1 从"整块页面"到"搭积木"的思维切换很多小白一开始写Vue,脑子里还是传统网页开发的模式:一个页面就是一个大文件,从上到下堆结构、堆样式、堆逻辑。模板写了一大段,data里挂着十几个变量,methods里躺着二十多个函数,页面稍微复杂一点,文件就到上千行,改一个按钮可能要在文件里来回滚半天。组件化要解决的,恰恰就是这个痛点。你可以把页面拆成一块一块独立的积木,每块积木有自己的结构、自己的样式、自己的逻辑,互不干扰。比如一个电商首页,可以拆成头部导航、轮播图、商品列表、购物车入口、底部Tab栏,每个部分独立成一个组件,组合在一起就是一个完整页面。这个思路在真实项目中非常实用。我在某公司做项目评审时见过一个刚转正的前端同事,把整个后台管理页面全写在一个单文件组件里,结果需求每次变更,他都要在那个文件里大海捞针。后来花了一个下午把页面拆成表格组件、筛选表单组件、分页组件,他自己都说,改动范围一下子缩小到原来的十分之一。Vue的组件化还有一个好处:组件是自带边界的。只要约定了对外暴露的数据和事件,内部怎么实现几乎不影响外部使用。这就意味着一个团队里,A同学负责商品卡片组件,B同学负责购物车组件,两个人可以并行开发,最后把组件一拼就能跑通。这种协作方式,和传统一人改一处、全局受牵连的开发模式比起来,效率差别不是一点半点。1.2 组件封装的第一条铁律:data必须是一个函数初学者最容易犯的第一个组件层面的错误,就是用对象形式写data。在Vue 2时代,根实例的data可以是对象,但组件里必须写成函数,否则多个组件实例会共享同一份数据,改一个全变。Vue 3组合式API里虽然变成了ref和reactive,但"每个组件实例的数据必须独立"这个原则没有变。举个例子:// 错误示范 data() { return { count: 0 } } // 这样写就是错的: data() { count: 0 }你可能觉得这不就是少了return吗?但组件被复用时,每个实例都需要一份自己的count。写成函数,每次调用data()都会返回一个全新的对象;直接写对象字面量的话,所有实例拿到的都是同一个内存地址。这个细节,就是组件化和小白之前写页面最大的不同——你不再只有一个页面实例,而是可能同时存在几十个组件实例,数据隔离必须在一开始就做好。我在实际教学和带人过程中,发现很多人会在这里栽跟头,但奇怪的是,报错往往不明显,表现是"我改了A组件的count,B组件怎么也跟着变了"。排查很久才发现是data没写成函数。所以这一条值得刻在脑子里。2. 第一个组件和父子组件的组合方式2.1 用defineProps给组件传参今天要掌握的第一块硬核内容,是父组件怎么把数据传给子组件。官方叫法是通过props传递属性。这个概念可以理解成:父组件给子组件递了一张纸条,纸条上写好了子组件需要显示的内容。看一个最简示例,一个展示用户信息的子组件:<!-- UserCard.vue --> <template> <div class="user-card"> <h3>{{ name }}</h3> <p>{{ email }}</p> </div> </template> <script setup> const props = defineProps({ name: { type: String, required: true }, email: { type: String, default: '暂无邮箱' } }) </script>父组件里这样使用:<template> <UserCard name="张三" email="zhangsan@example.com" /> </template> <script setup> import UserCard from './UserCard.vue' </script>defineProps是Vue 3组合式API里专门用来声明props的编译宏,不需要手动引入,直接在<script setup>里用就行。声明的时候可以用简单数组形式(defineProps(['name', 'email'])),也可以用对象形式带上类型、默认值、必填校验。实际开发中我建议用对象形式,理由很简单:三五个props的时候数组写法看着清爽,等组件复杂到十几个props时,没有类型校验和默认值兜底,调用方稍不注意就会传错,排查起来非常痛苦。2.2 父子组件的嵌套与插槽有了props,父子组件之间能传数据了,但还有一个场景:父组件想往子组件里塞一段自己的模板内容。比如一个通用弹窗组件,弹窗的边框、关闭按钮、背景遮罩都是公共的,但弹窗里面是什么,每个页面都不一样。这种"骨架相同、内容不同"的需求,用slot来解决。<!-- Modal.vue --> <template> <div class="modal-mask"> <div class="modal-content"> <slot></slot> <button @click="$emit('close')">关闭</button> </div> </div> </template>使用方:<Modal @close="showModal = false"> <p>这里的内容完全由父组件控制</p> </Modal><slot></slot>像一个占位符,父组件塞进来的内容最终会渲染到这个位置。这就是组件插槽的魅力——它让组件的复用性又上了一个台阶。我对小白的建议是,先掌握默认插槽,把具名插槽、作用域插槽放到后面再深入,Day5能理解默认插槽的机制就够了,否则信息量太大容易劝退。2.3 组件拆分到什么粒度才合适这个问题没有绝对标准,但有一点可以分享:如果一个组件模板超过80行、逻辑里职责超过一件事,就值得考虑拆分了。我见过有的项目把整个登录页拆成一个组件文件,几百行代码塞在一起,虽然也跑得动,但维护性其实和没拆一样。反过来,如果把一个按钮的样式都拆成组件,那就过度设计了,组件数量爆炸,反而拖累开发效率。一个简单实用的判断标准:单个组件是否可以被多次复用,或者是否承担了明确的单一职责。比如"用户头像"组件,可能在导航栏、评论区、消息列表里都用得上,值得拆;"登录表单"里的用户名输入框,只在这个表单里出现,就没必要单独拆。Day5你只需要建立"该拆就拆、不该拆别硬拆"的基本感觉,等写过几个项目,这个粒度判断自然就熟练了。3. 子组件和父组件如何对话3.1 用defineEmits发送事件数据从父到子,靠props;数据从子到父,靠事件。这就是Vue组件通信的"单向数据流"原则:父组件可以给子组件数据,但子组件不能直接修改父组件的值,只能通过触发事件通知父组件"你去改"。<!-- ChildCounter.vue --> <template> <button @click="increase">+1</button> </template> <script setup> const emit = defineEmits(['increase']) function increase() { emit('increase', 1) } </script>父组件:<template> <ChildCounter @increase="handleIncrease" /> <p>当前计数:{{ count }}</p> </template> <script setup> import { ref } from 'vue' import ChildCounter from './ChildCounter.vue' const count = ref(0) function handleIncrease(num) { count.value += num } </script>这里子组件并没有直接去改父组件的count,而是通过emit('increase', 1)告诉父组件"我这边发生了增加事件,附带数据是1"。父组件监听@increase,决定接下来怎么处理。这个过程,像极了实际工作中下级向上级汇报,但决定权始终在上级手里。之所以设计成这种单向模式,是因为如果允许子组件随意改父组件的数据,那数据流向就乱了,一个页面几十个组件互相改来改去,排查一个bug需要翻遍所有组件,谁开发谁知道那个痛苦。3.2 自定义事件的事件名规范我在Review代码时发现,很多人习惯把事件名写成驼峰式,比如emit('increaseCount'),父组件里也监听@increaseCount。这在Vue 3里能用,但官方推荐的是kebab-case,即emit('increase-count'),父组件写成@increase-count。原因是HTML标签对大小写不敏感,模板里写@increaseCount,浏览器解析时可能会把它当成@increasecount,导致事件对不上。虽然Vue内部做了兼容处理,但这种"可能踩坑但不确定什么时候踩"的问题,最好从源头避免。3.3 用v-model实现父子双向绑定props加emits已经能覆盖大部分通信场景,但有一种特殊情况值得单独讲:表单项。比如一个输入框组件,父组件需要知道用户输入了什么,同时还要能外部重置输入框的值,这就涉及真正的"双向绑定"。Vue 3里自定义组件支持v-model,本质上是modelValue这个props加update:modelValue这个事件的语法糖。下面这个输入框组件就是标准写法:<!-- MyInput.vue --> <template> <input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" /> </template> <script setup> defineProps(['modelValue']) const emit = defineEmits(['update:modelValue']) </script>父组件:<MyInput v-model="username" />这一行v-model展开后,等价于:<MyInput :modelValue="username" @update:modelValue="username = $event" />也就是说,子组件负责通知父组件"输入框的内容变了",父组件更新username后,再把新值通过modelValue传回子组件。看起来绕了一圈,但核心逻辑没变:数据流的源头和终点始终在父组件手里。对于小白来说,Day5先把这个标准写法记住,知道它是props加emits的组合即可,后续要在组件里封装表单控件,这个基础就够用了。3.4 一个常见误区:直接修改props和"data必须是函数"齐名的另一个坑,就是子组件内部直接修改props的值。比如这样:<!-- 错误示范 --> <script setup> const props = defineProps(['visible']) props.visible = false // 报错或警告 </script>Vue会给出警告,严格模式下会直接报错。原因前面已经说过了,props是父组件传给子组件的"只读快照",子组件没有权限修改。正确做法是:要么触发事件让父组件改,要么把props值复制一份到子组件内部状态,再在本地修改。这个限制会让很多从原生JS转过来的人觉得麻烦,但它正是Vue组件化维护性的基石——数据流向清晰可追踪,出错时能顺着箭头很快定位。4. 组件生命周期和你的第一道实战坑4.1 四个最常用的生命周期钩子Vue组件的生命周期钩子,就是组件从创建到销毁过程中特定时机触发的事件。Day5不要求把所有钩子背下来,但至少要掌握四个:onMounted、onUpdated、onBeforeUnmount、onUnmounted。<script setup> import { onMounted, onUpdated, onBeforeUnmount, onUnmounted } from 'vue' onMounted(() => { // 组件挂载到页面上之后 // 适合做接口请求、DOM操作、定时器初始化 }) onUpdated(() => { // 组件因数据变化重新渲染之后 }) onBeforeUnmount(() => { // 组件即将卸载,清理定时器、取消事件监听的好时机 }) onUnmounted(() => { // 组件已经卸载 }) </script>初学阶段,onMounted用的频率最高,因为小白第一批实战项目里,基本都要在页面加载后去请求数据。但这里有个隐藏的坑:如果你在onMounted里初始化了一个定时器,却忘了清理它,组件卸载后定时器依然在跑,会引发一系列诡异问题——比如页面已经切走了,控制台还在报错,甚至内存泄漏。这个问题的典型解法就是在onBeforeUnmount或onUnmounted里clearInterval。4.2 一个真实的小白排查实录我自己带过一位刚入门的朋友,他写了个列表页,需求是点击按钮打开一个弹窗,弹窗里展示当前选中项的数据。他做法是:点按钮时,弹窗组件的props接收到当前数据,然后弹窗组件内部在onMounted里发起数据请求。表面看起来没问题,但实际跑起来,第一次打开弹窗数据正常,第二次打开弹窗数据还是旧的。排查了一圈,原因很简单:弹窗组件没有被销毁,它只是被v-if控制显隐。同一个组件实例,在第一次打开后挂载,第二次打开时压根不会重新触发onMounted,自然也就不会重新请求数据。这就是我前面说的"组件生命周期"和"页面生命周期"的差别——你以为弹窗重新渲染了,但实际上它还是那个组件,状态一直留着。解决办法有两种。第一种,用v-if让弹窗每次都重建,挂载钩子每次都会触发;第二种,改用watch监听props的变化,在数据更新时重新请求。前者简单直接,后者更接近真实项目的常用做法。<script setup> import { watch } from 'vue' const props = defineProps(['currentItem']) watch(() => props.currentItem, (newVal) => { // 每次传入的currentItem变化时,重新拉取详情 }) </script>这个案例很典型,我经常拿它来说说明白"组件挂载不等于页面加载"这件事。很多小白的思维惯性是"打开页面=挂载",一旦遇到弹窗、抽屉这种不销毁的组件,就容易踩到生命周期不会重复触发的坑。4.3 常见问题速查表现象可能原因排查方向组件数据改了但页面没变修改了props而不是声明响应式数据检查是否直接改了props,用ref或reactive声明本地数据弹窗第二次打开数据是旧的组件实例未销毁,onMounted不重复触发用v-if重建组件,或watch监听触发更新多个相同组件互相影响data()没有写成函数,或组合式API里复用了同一个ref确保每个组件都有独立的数据实例子组件事件触发了但父组件没收到事件名大小写或命名不一致统一用kebab-case命名并检查defineEmits声明定时器在页面切换后一直报错卸载时没清理定时器在onBeforeUnmount里clearInterval或clearTimeout列表渲染时每项组件需要不同状态直接用了循环索引作为组件唯一标识给每项传入唯一的key,不要用index表格里最后一条我要特别说一句:Vue依赖key来区分列表中的每一项,很多时候大家在列表渲染时图省事直接用index做key,一旦列表发生增删、排序,组件状态就会出现串位。如果你在给列表项做复选框、输入框这类带交互的组件,这个坑几乎必踩。正确做法是用数据自带的唯一字段,比如用户ID、订单号,实在没有的话,可以用一个自增的ID生成函数,总之别用index。5. 我的实操心得:Day5阶段应该怎么练光看不练,组件化思维是建立不起来的。我给初学者的建议是:不要急着去做大型项目,而是找你手头已经写过的某个页面,把它拆成至少三个组件,哪怕只是"页头、内容区、页脚"这种最朴素的分法,也能让你体会到组件化的意义。拆完之后,试着把某些数据从父组件传给子组件,再试着让子组件在某个事件触发时通知父组件。这比我布置100道填空题都管用。还有一个好用的练习法,是"组件接口契约"练习。你定义一个组件,先声明它接收哪些props、暴露哪些事件,然后问自己:如果别人要用这个组件,他需要知道什么信息?先把这个"使用说明"写出来,再去实现内部逻辑。这个习惯能倒逼你把组件设计得干净清晰,而不是写着写着就什么都往一个文件里塞。另外,建议从Day5开始,建立自己的"组件问题清单"。遇到一个坑,就把它记下来,后面再碰到相同症状就直接查表。我自己早年写前端时就有这个习惯,很多经验不是靠天赋,而是靠记录和复盘慢慢积累起来的。等到组件通信、生命周期、插槽这些概念都熟练了,再回看Day5这一天的内容,你会发现自己已经在不知不觉中跨过了"写页面"到"写应用"的分水岭。
很多初学者学Vue,前四天还跟得上节奏,一到第五天就有点懵——因为从这一天开始,你不再只是"写页面",而是开始写"组件"了。我自己的感受是:模板语法、指令、状态管理这些学起来都还算直观,真正让脑子转个弯的,恰恰是组件化这一关。Day5这个节点,就是把"页面思维"切换到"组件思维"的关键一天。这篇文章就围绕第五天的核心内容展开,把组件化这条主线讲透,配合能直接跑的代码示例和踩坑记录,帮还在组件门口徘徊的朋友顺利迈过去。1. 组件化到底改变了什么1.1 从"整块页面"到"搭积木"的思维切换很多小白一开始写Vue,脑子里还是传统网页开发的模式:一个页面就是一个大文件,从上到下堆结构、堆样式、堆逻辑。模板写了一大段,data里挂着十几个变量,methods里躺着二十多个函数,页面稍微复杂一点,文件就到上千行,改一个按钮可能要在文件里来回滚半天。组件化要解决的,恰恰就是这个痛点。你可以把页面拆成一块一块独立的积木,每块积木有自己的结构、自己的样式、自己的逻辑,互不干扰。比如一个电商首页,可以拆成头部导航、轮播图、商品列表、购物车入口、底部Tab栏,每个部分独立成一个组件,组合在一起就是一个完整页面。这个思路在真实项目中非常实用。我在某公司做项目评审时见过一个刚转正的前端同事,把整个后台管理页面全写在一个单文件组件里,结果需求每次变更,他都要在那个文件里大海捞针。后来花了一个下午把页面拆成表格组件、筛选表单组件、分页组件,他自己都说,改动范围一下子缩小到原来的十分之一。Vue的组件化还有一个好处:组件是自带边界的。只要约定了对外暴露的数据和事件,内部怎么实现几乎不影响外部使用。这就意味着一个团队里,A同学负责商品卡片组件,B同学负责购物车组件,两个人可以并行开发,最后把组件一拼就能跑通。这种协作方式,和传统一人改一处、全局受牵连的开发模式比起来,效率差别不是一点半点。1.2 组件封装的第一条铁律:data必须是一个函数初学者最容易犯的第一个组件层面的错误,就是用对象形式写data。在Vue 2时代,根实例的data可以是对象,但组件里必须写成函数,否则多个组件实例会共享同一份数据,改一个全变。Vue 3组合式API里虽然变成了ref和reactive,但"每个组件实例的数据必须独立"这个原则没有变。举个例子:// 错误示范 data() { return { count: 0 } } // 这样写就是错的: data() { count: 0 }你可能觉得这不就是少了return吗?但组件被复用时,每个实例都需要一份自己的count。写成函数,每次调用data()都会返回一个全新的对象;直接写对象字面量的话,所有实例拿到的都是同一个内存地址。这个细节,就是组件化和小白之前写页面最大的不同——你不再只有一个页面实例,而是可能同时存在几十个组件实例,数据隔离必须在一开始就做好。我在实际教学和带人过程中,发现很多人会在这里栽跟头,但奇怪的是,报错往往不明显,表现是"我改了A组件的count,B组件怎么也跟着变了"。排查很久才发现是data没写成函数。所以这一条值得刻在脑子里。2. 第一个组件和父子组件的组合方式2.1 用defineProps给组件传参今天要掌握的第一块硬核内容,是父组件怎么把数据传给子组件。官方叫法是通过props传递属性。这个概念可以理解成:父组件给子组件递了一张纸条,纸条上写好了子组件需要显示的内容。看一个最简示例,一个展示用户信息的子组件:<!-- UserCard.vue --> <template> <div class="user-card"> <h3>{{ name }}</h3> <p>{{ email }}</p> </div> </template> <script setup> const props = defineProps({ name: { type: String, required: true }, email: { type: String, default: '暂无邮箱' } }) </script>父组件里这样使用:<template> <UserCard name="张三" email="zhangsan@example.com" /> </template> <script setup> import UserCard from './UserCard.vue' </script>defineProps是Vue 3组合式API里专门用来声明props的编译宏,不需要手动引入,直接在<script setup>里用就行。声明的时候可以用简单数组形式(defineProps(['name', 'email'])),也可以用对象形式带上类型、默认值、必填校验。实际开发中我建议用对象形式,理由很简单:三五个props的时候数组写法看着清爽,等组件复杂到十几个props时,没有类型校验和默认值兜底,调用方稍不注意就会传错,排查起来非常痛苦。2.2 父子组件的嵌套与插槽有了props,父子组件之间能传数据了,但还有一个场景:父组件想往子组件里塞一段自己的模板内容。比如一个通用弹窗组件,弹窗的边框、关闭按钮、背景遮罩都是公共的,但弹窗里面是什么,每个页面都不一样。这种"骨架相同、内容不同"的需求,用slot来解决。<!-- Modal.vue --> <template> <div class="modal-mask"> <div class="modal-content"> <slot></slot> <button @click="$emit('close')">关闭</button> </div> </div> </template>使用方:<Modal @close="showModal = false"> <p>这里的内容完全由父组件控制</p> </Modal><slot></slot>像一个占位符,父组件塞进来的内容最终会渲染到这个位置。这就是组件插槽的魅力——它让组件的复用性又上了一个台阶。我对小白的建议是,先掌握默认插槽,把具名插槽、作用域插槽放到后面再深入,Day5能理解默认插槽的机制就够了,否则信息量太大容易劝退。2.3 组件拆分到什么粒度才合适这个问题没有绝对标准,但有一点可以分享:如果一个组件模板超过80行、逻辑里职责超过一件事,就值得考虑拆分了。我见过有的项目把整个登录页拆成一个组件文件,几百行代码塞在一起,虽然也跑得动,但维护性其实和没拆一样。反过来,如果把一个按钮的样式都拆成组件,那就过度设计了,组件数量爆炸,反而拖累开发效率。一个简单实用的判断标准:单个组件是否可以被多次复用,或者是否承担了明确的单一职责。比如"用户头像"组件,可能在导航栏、评论区、消息列表里都用得上,值得拆;"登录表单"里的用户名输入框,只在这个表单里出现,就没必要单独拆。Day5你只需要建立"该拆就拆、不该拆别硬拆"的基本感觉,等写过几个项目,这个粒度判断自然就熟练了。3. 子组件和父组件如何对话3.1 用defineEmits发送事件数据从父到子,靠props;数据从子到父,靠事件。这就是Vue组件通信的"单向数据流"原则:父组件可以给子组件数据,但子组件不能直接修改父组件的值,只能通过触发事件通知父组件"你去改"。<!-- ChildCounter.vue --> <template> <button @click="increase">+1</button> </template> <script setup> const emit = defineEmits(['increase']) function increase() { emit('increase', 1) } </script>父组件:<template> <ChildCounter @increase="handleIncrease" /> <p>当前计数:{{ count }}</p> </template> <script setup> import { ref } from 'vue' import ChildCounter from './ChildCounter.vue' const count = ref(0) function handleIncrease(num) { count.value += num } </script>这里子组件并没有直接去改父组件的count,而是通过emit('increase', 1)告诉父组件"我这边发生了增加事件,附带数据是1"。父组件监听@increase,决定接下来怎么处理。这个过程,像极了实际工作中下级向上级汇报,但决定权始终在上级手里。之所以设计成这种单向模式,是因为如果允许子组件随意改父组件的数据,那数据流向就乱了,一个页面几十个组件互相改来改去,排查一个bug需要翻遍所有组件,谁开发谁知道那个痛苦。3.2 自定义事件的事件名规范我在Review代码时发现,很多人习惯把事件名写成驼峰式,比如emit('increaseCount'),父组件里也监听@increaseCount。这在Vue 3里能用,但官方推荐的是kebab-case,即emit('increase-count'),父组件写成@increase-count。原因是HTML标签对大小写不敏感,模板里写@increaseCount,浏览器解析时可能会把它当成@increasecount,导致事件对不上。虽然Vue内部做了兼容处理,但这种"可能踩坑但不确定什么时候踩"的问题,最好从源头避免。3.3 用v-model实现父子双向绑定props加emits已经能覆盖大部分通信场景,但有一种特殊情况值得单独讲:表单项。比如一个输入框组件,父组件需要知道用户输入了什么,同时还要能外部重置输入框的值,这就涉及真正的"双向绑定"。Vue 3里自定义组件支持v-model,本质上是modelValue这个props加update:modelValue这个事件的语法糖。下面这个输入框组件就是标准写法:<!-- MyInput.vue --> <template> <input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" /> </template> <script setup> defineProps(['modelValue']) const emit = defineEmits(['update:modelValue']) </script>父组件:<MyInput v-model="username" />这一行v-model展开后,等价于:<MyInput :modelValue="username" @update:modelValue="username = $event" />也就是说,子组件负责通知父组件"输入框的内容变了",父组件更新username后,再把新值通过modelValue传回子组件。看起来绕了一圈,但核心逻辑没变:数据流的源头和终点始终在父组件手里。对于小白来说,Day5先把这个标准写法记住,知道它是props加emits的组合即可,后续要在组件里封装表单控件,这个基础就够用了。3.4 一个常见误区:直接修改props和"data必须是函数"齐名的另一个坑,就是子组件内部直接修改props的值。比如这样:<!-- 错误示范 --> <script setup> const props = defineProps(['visible']) props.visible = false // 报错或警告 </script>Vue会给出警告,严格模式下会直接报错。原因前面已经说过了,props是父组件传给子组件的"只读快照",子组件没有权限修改。正确做法是:要么触发事件让父组件改,要么把props值复制一份到子组件内部状态,再在本地修改。这个限制会让很多从原生JS转过来的人觉得麻烦,但它正是Vue组件化维护性的基石——数据流向清晰可追踪,出错时能顺着箭头很快定位。4. 组件生命周期和你的第一道实战坑4.1 四个最常用的生命周期钩子Vue组件的生命周期钩子,就是组件从创建到销毁过程中特定时机触发的事件。Day5不要求把所有钩子背下来,但至少要掌握四个:onMounted、onUpdated、onBeforeUnmount、onUnmounted。<script setup> import { onMounted, onUpdated, onBeforeUnmount, onUnmounted } from 'vue' onMounted(() => { // 组件挂载到页面上之后 // 适合做接口请求、DOM操作、定时器初始化 }) onUpdated(() => { // 组件因数据变化重新渲染之后 }) onBeforeUnmount(() => { // 组件即将卸载,清理定时器、取消事件监听的好时机 }) onUnmounted(() => { // 组件已经卸载 }) </script>初学阶段,onMounted用的频率最高,因为小白第一批实战项目里,基本都要在页面加载后去请求数据。但这里有个隐藏的坑:如果你在onMounted里初始化了一个定时器,却忘了清理它,组件卸载后定时器依然在跑,会引发一系列诡异问题——比如页面已经切走了,控制台还在报错,甚至内存泄漏。这个问题的典型解法就是在onBeforeUnmount或onUnmounted里clearInterval。4.2 一个真实的小白排查实录我自己带过一位刚入门的朋友,他写了个列表页,需求是点击按钮打开一个弹窗,弹窗里展示当前选中项的数据。他做法是:点按钮时,弹窗组件的props接收到当前数据,然后弹窗组件内部在onMounted里发起数据请求。表面看起来没问题,但实际跑起来,第一次打开弹窗数据正常,第二次打开弹窗数据还是旧的。排查了一圈,原因很简单:弹窗组件没有被销毁,它只是被v-if控制显隐。同一个组件实例,在第一次打开后挂载,第二次打开时压根不会重新触发onMounted,自然也就不会重新请求数据。这就是我前面说的"组件生命周期"和"页面生命周期"的差别——你以为弹窗重新渲染了,但实际上它还是那个组件,状态一直留着。解决办法有两种。第一种,用v-if让弹窗每次都重建,挂载钩子每次都会触发;第二种,改用watch监听props的变化,在数据更新时重新请求。前者简单直接,后者更接近真实项目的常用做法。<script setup> import { watch } from 'vue' const props = defineProps(['currentItem']) watch(() => props.currentItem, (newVal) => { // 每次传入的currentItem变化时,重新拉取详情 }) </script>这个案例很典型,我经常拿它来说说明白"组件挂载不等于页面加载"这件事。很多小白的思维惯性是"打开页面=挂载",一旦遇到弹窗、抽屉这种不销毁的组件,就容易踩到生命周期不会重复触发的坑。4.3 常见问题速查表现象可能原因排查方向组件数据改了但页面没变修改了props而不是声明响应式数据检查是否直接改了props,用ref或reactive声明本地数据弹窗第二次打开数据是旧的组件实例未销毁,onMounted不重复触发用v-if重建组件,或watch监听触发更新多个相同组件互相影响data()没有写成函数,或组合式API里复用了同一个ref确保每个组件都有独立的数据实例子组件事件触发了但父组件没收到事件名大小写或命名不一致统一用kebab-case命名并检查defineEmits声明定时器在页面切换后一直报错卸载时没清理定时器在onBeforeUnmount里clearInterval或clearTimeout列表渲染时每项组件需要不同状态直接用了循环索引作为组件唯一标识给每项传入唯一的key,不要用index表格里最后一条我要特别说一句:Vue依赖key来区分列表中的每一项,很多时候大家在列表渲染时图省事直接用index做key,一旦列表发生增删、排序,组件状态就会出现串位。如果你在给列表项做复选框、输入框这类带交互的组件,这个坑几乎必踩。正确做法是用数据自带的唯一字段,比如用户ID、订单号,实在没有的话,可以用一个自增的ID生成函数,总之别用index。5. 我的实操心得:Day5阶段应该怎么练光看不练,组件化思维是建立不起来的。我给初学者的建议是:不要急着去做大型项目,而是找你手头已经写过的某个页面,把它拆成至少三个组件,哪怕只是"页头、内容区、页脚"这种最朴素的分法,也能让你体会到组件化的意义。拆完之后,试着把某些数据从父组件传给子组件,再试着让子组件在某个事件触发时通知父组件。这比我布置100道填空题都管用。还有一个好用的练习法,是"组件接口契约"练习。你定义一个组件,先声明它接收哪些props、暴露哪些事件,然后问自己:如果别人要用这个组件,他需要知道什么信息?先把这个"使用说明"写出来,再去实现内部逻辑。这个习惯能倒逼你把组件设计得干净清晰,而不是写着写着就什么都往一个文件里塞。另外,建议从Day5开始,建立自己的"组件问题清单"。遇到一个坑,就把它记下来,后面再碰到相同症状就直接查表。我自己早年写前端时就有这个习惯,很多经验不是靠天赋,而是靠记录和复盘慢慢积累起来的。等到组件通信、生命周期、插槽这些概念都熟练了,再回看Day5这一天的内容,你会发现自己已经在不知不觉中跨过了"写页面"到"写应用"的分水岭。