ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Vue 3组合式API与生命周期函数:从setup到组件通信的实战指南

Vue 3组合式API与生命周期函数:从setup到组件通信的实战指南 上一章我们把组件的基础概念、模板语法和样式作用域过了一遍这一章开始进入 Vue 3 真正“重头戏”的部分组合式 APIComposition API和生命周期函数。理解这两个东西基本就拿到了 Vue 3 组件开发的入场券。很多刚从 Options API 转过来的同学会觉得很别扭其实不用急组合式 API 并没有抛弃 Vue 的响应式、模板语法和组件设计思路它只是把“按选项组织代码”改成了“按逻辑组织代码”。我会结合自己实际开发中遇到的情况把这章内容尽量讲透顺带把生命周期函数从创建、挂载、更新到销毁的整套流程梳理清楚看完这一篇你再回去看项目里的业务组件会有完全不一样的感觉。这一章适合所有想把 Vue 3 用明白的人不管是刚学完基础语法的新手还是从 Vue 2 迁过来的老开发都建议认真过一遍。我会把 setup 语法糖、ref 和 reactive 的选择、onMounted 这类钩子的使用时机、以及组件通信的底层逻辑一起串起来讲让它们不再是孤立的知识点而是一套能直接落到项目里的开发方法论。1. 组合式 API 到底解决了什么问题1.1 Options API 的痛点与新方案的诞生在 Vue 2 时代我们写组件基本是固定在 data、computed、methods、watch、mounted 这样的“格子”里。组件简单的时候还好一旦业务复杂同一个功能的代码会被拆散到各个选项中比如某个搜索功能它的数据在 data 里请求逻辑在 methods 里监听器在 watch 里初始化在 mounted 里。当你要维护这段功能时得在各个选项之间来回跳这还不算最痛苦最痛苦的是多个功能混在一起时代码可读性直线下降。我接手过一个老项目一个表格页面里有搜索、筛选、分页、导出、批量操作五六套逻辑全部堆在一个几百行的 Vue 2 组件里。每次改需求我得先花半小时把代码在脑子里“重新组装”一遍才知道哪段 data 对应哪段 methods。这就是 Options API 在大型组件里的固有缺陷按选项强制分块而不是按逻辑归属分块。组合式 API 的思路就是反过来的它让你可以把同一个功能的数据、计算属性、方法、监听器全部写在一个“块”里。比如做搜索功能你就把 searchKeyword、searchResult、handleSearch 写在一起做分页就再写一个分页块两个功能之间互不干扰改起来也不用到处跳。除了代码组织组合式 API 还有一个巨大优势逻辑复用。Vue 2 时代做逻辑复用主要靠 mixin但 mixin 的毛病大家都懂命名冲突、数据来源不透明、多个 mixin 叠加时根本不知道某个属性来自哪里。组合式 API 里的自定义 hook也就是用函数封装逻辑天然避开了这些问题因为一个函数就是一个独立的作用域参数传递明确返回什么一目了然。1.2 setup 函数的定位与执行时机要说组合式 API必须先说 setup。它是组合式 API 的入口函数所有的组合式逻辑都在 setup 里完成。但很多初学者第一次看到 setup 会懵它到底什么时候执行跟 data、mounted 这些是什么关系搞清楚这点很关键。setup 是在组件实例创建之前执行的执行时机比 beforeCreate 还要早。也就是说在这个阶段组件实例还不存在你访问不到 this也访问不到 data、computed、methods 里的内容。这也解释了为什么在 setup 里不能再用 this.xxx 这种写法了不是因为 Vue 3 删了 this而是 setup 执行时实例压根还没初始化完。如果你用模板语法也就是我们在 Vue 3 里最常用的script setup那 setup 就是组件的顶层作用域相当于把整个组件的逻辑直接写在 setup 函数体里。日常开发中我基本都是用script setup它比手动写 setup 函数再 return 一堆东西要清爽得多少写很多样板代码。关于 setup 的执行时机还有一点值得注意setup 同步代码执行时组件还没有挂载到 DOM 上。所以你在 setup 里直接操作 DOM 节点是拿不到的要想操作 DOM得等 onMounted 之类的生命周期钩子触发之后再做。这个顺序问题我后面细讲这里先有个印象就行。2. 组合式 API 的核心构成ref、reactive、computed 与 watch2.1 ref 和 reactive 怎么选要讲组合式 API 就绕不开响应式数据。Vue 3 里创建响应式数据最基础的两个 API 就是 ref 和 reactive。很多新手一开始会纠结到底用哪个我直接给结论绝大多数场景用 ref这没有任何问题。一个价值观很朴素的理由是ref 写起来更统一不管是基本类型还是对象类型都通过.value来读写逻辑上不用来回切换。ref 的核心原理是把原始数据包装成一个带 value 属性的响应式对象。你在 JS 里访问它必须写.value但在模板里 Vue 会自动帮你解包直接写变量名就行。这一点很容易踩坑我会在后面的常见问题里专门讲。reactive 则是直接把一个对象变成响应式对象不需要.value读属性直接 obj.xxx。它更适合管理一个“结构化”的数据集合比如表单对象、页面筛选条件。但 reactive 有两个比较麻烦的限制第一它只能用于对象、数组这类引用类型基本类型不适用第二如果你直接对 reactive 对象整体重新赋值比如state {...}会丢失响应式因为赋值操作把原来的代理对象替换掉了。这个问题在工作中特别容易遇到很多人排查半天才发现是这里出了问题。我的个人习惯是组件内部零散的数据、表单字段、简单对象一律 ref如果是聚合型的数据结构比如一个包含若干子对象和数组的配置对象用 reactive 会更顺手。但说实话全用 ref 也完全可行甚至团队规范里统一用 ref 会更省心因为心智负担小。2.2 computed 在组合式 API 里的用法computed 在组合式 API 里就是个函数接收一个 getter返回一个“不可直接修改的响应式 ref”。大多数人用它最多的场景就是派生数据比如购物车总价、筛选后的列表、用户的完整名称拼接。这里想多说一句的是computed 是会缓存结果的它只在依赖的响应式数据发生变化时才重新计算。这跟 methods 里的普通函数不一样后者每次渲染都会执行。所以如果某个值是多个响应式数据经过计算得来的优先用 computed不要在模板里写复杂表达式也不要用方法去算。模板里的表达式应该是“描述性”的而不是“逻辑性”的。在script setup里使用 computed 也很自然直接const total computed(...)模板里拿 total 就是计算后的值。注意不要在 computed 的 getter 里做副作用操作比如发请求、改 DOM这些行为应该放在 watch 或事件处理函数里computed 只负责“算”。2.3 watch 和 watchEffect一个监听一个自动感知watch 的用法和 Vue 2 差不多监听一个或多个响应式数据源数据变化时执行回调。组合式 API 里 watch 接收的第一个参数可以是 ref、reactive 的某个属性也可以是一个 getter 函数。跟 Vue 2 不同的一点是Vue 3 里如果你监听 reactive 对象的属性需要写成 getter 形式比如watch(() state.count, (newVal, oldVal) { console.log(count 变了, newVal, oldVal); });直接用watch(state.count, ...)是不行的因为state.count只是当前的值不是一个响应式引用watch 拿不到监听目标。这一点特别容易踩坑。watch 还有一个 immediate 选项设为 true 时会在初始化时立即执行一次回调适合“进入页面就要拉一次数据”的场景。默认情况下 watch 是懒执行的只在数据变化时触发。watchEffect 是 Vue 3 新增的 API它不指定监听谁而是自动追踪你回调函数里用到了哪些响应式数据只要用到的一变回调就重新执行。它非常适合做“联动”操作比如某个响应式数据变化后自动发请求、自动设置状态。但要注意watchEffect 一旦开始就会立即执行一次这一点跟带 immediate 的 watch 类似用的时候心里有数就行。3. 生命周期函数挂载顺序、对应关系和注意事项3.1 每个钩子的触发时机与对应关系Vue 3 的生命周期钩子和 Vue 2 基本一一对应只是命名发生了变化加了on前缀并且只能在 setup 内部同步调用。我把它们的对应关系整理成了一张表建议收藏组合式 APIVue 3选项式 APIVue 2/3 Options触发时机setup()beforeCreate / created组件实例创建前data 和 methods 还未初始化onBeforeMountbeforeMount组件挂载到 DOM 之前模板编译完成onMountedmounted组件挂载完成后可以访问 DOMonBeforeUpdatebeforeUpdate响应式数据变化重新渲染之前onUpdatedupdated重新渲染完成后DOM 已更新onBeforeUnmountbeforeDestroy组件卸载之前实例仍可用onUnmounteddestroyed组件卸载完成实例销毁onActivated / onDeactivatedactivated / deactivatedkeep-alive 组件激活 / 停用时onErrorCapturederrorCaptured后代组件抛出错误时这里想强调一下 setup 和 beforeCreate / created 的关系。在选项式写法里created 是最早执行的生命周期钩子而在组合式写法里setup 就相当于把 beforeCreate 和 created 一起取代了。你在 setup 里做的事情初始化数据、定义方法、创建监听就相当于 Vue 2 里在 created 里做的事情。所以我建议对旧项目做迁移时直接把原来 created 里的逻辑挪进 setup 的开头就行。3.2 setup 里写 onMounted 的细节在实际开发中onMounted 是我用得最多的钩子没有之一。页面数据请求、DOM 操作、图表初始化、第三方库的挂载基本都放在这里。但有几个细节容易忽略第一onMounted 里的回调函数可以随意使用 setup 作用域里定义的变量因为闭包机制会捕获它们。而且这个作用域关系是在注册时就已经确定的不受执行顺序影响。第二setup 里可以注册多个 onMounted。Vue 会按照注册顺序依次执行。这个特性很适合把不同的初始化逻辑拆开写比如onMounted(() { // 拉列表数据 fetchList(); }); onMounted(() { // 初始化图表 initChart(); });两个初始化逻辑互不干扰代码清爽也方便单测时单独验证。第三一个非常重要的点不要在 onMounted 里再嵌套引入副作用但又不同步执行业务逻辑。很多人习惯在 onMounted 里写axios.get(...).then(...)这没问题。但如果你在 then 回调里去 setState 修改响应式数据要注意这个更新是异步的渲染更新不一定立刻发生。需要“拿到数据后立刻操作 DOM”的场景必须等 DOM 更新完成这时候用 nextTick 才会稳妥。3.3 onUpdated 和 onBeforeUnmount常被忽略但其实很有用很多人对 onUpdated 没什么好感因为它会在每次数据变化重新渲染后触发频繁得让人想不起来用。但有两类场景它很关键一是需要“根据 DOM 变化做后续处理”的时候比如第三方插件需要在 DOM 结构变化后重算尺寸二是调试的时候想确认某次数据更新到底有没有导致组件重新渲染。不过我还是建议谨慎使用 onUpdated因为频繁触发的情况下容易连带产生性能问题。如果只是想监听某个具体数据的变化用 watch 更精准不会因为无关数据变化而白白执行回调。onBeforeUnmount 则是清理“现场”的关键钩子。定时器、事件监听、第三方实例、WebSocket 连接这些都要在这里清理。我见过不少线上 bug 就是定时器没清、事件没解绑导致组件销毁后回调还在执行控制台报错、内存泄漏、接口重复请求各种怪象都来了。写组件时养成一个好习惯凡是在 setup 里创建的定时器、监听器、订阅器都要对应在 onBeforeUnmount 里清理。这对长列表页面、弹窗组件、频繁切换的路由页面尤其重要。4. 组件通信基础从手动传值到响应式同步4.1 父子组件通信props 下传、事件上抛说完了生命周期得把组件通信的基础补上。在一个组件系统里几乎没有哪个组件是完全独立的总要有数据从父组件传进来或者把内部状态变化告诉父组件。Vue 3 中最常见也是最基础的通信方式就是 props 和自定义事件。父传子很简单在子组件里用 defineProps 声明接收的属性然后在父组件模板里通过绑定方式传入。这里要注意 defineProps 接收的类型和默认值最好都写清楚既能当文档用又能在编译期尽早发现问题!-- 子组件 -- script setup const props defineProps({ title: { type: String, required: true }, count: { type: Number, default: 0 } }); /script子传父则靠 emit。先用 defineEmits 声明事件名称再在适当时候调用传参给父组件。这里有一个新手很容易犯的错误把 props 直接拿来修改比如在子组件里写props.count。这是不被允许也不建议的props 是单向数据流子组件不应该去改父组件传下来的值。如果确实需要修改正确的做法是复制一份到本地数据或者干脆把它交还给父组件去改。对于“半受控”的组件我一般会在本地维护一个变量用 watch 同步 props 的变化再通过 emit 把更新抛回父组件形成一种“单向往下传、事件往回抛”的数据流闭环。这是 Vue 组件通信的精髓理解了这层后面再去看状态管理工具就轻松很多。4.2 non-props 属性和 $attrs透传背后的设计逻辑还有一个很多人容易忽略的点那些没有在 defineProps 里声明的属性最终会被放在 $attrs 上并且默认透传到子组件的最外层元素上。比如你给一个按钮组件传了一个 class但组件内部没有用 props 接收 class它依然会应用到按钮根元素上。从项目层面看这个机制非常实用。它保证了组件的“可扩展性”你不需要为每个可能的 HTML 属性都声明一遍 props只要组件没有吃掉它它就能自动往下传。不过如果根元素不想接收这些透传属性可以通过defineOptions({ inheritAttrs: false })关闭自动继承然后手动决定把它们放到哪里。我在封装表单控件、弹窗这类组件时经常用到这个技巧因为它能让组件内部结构更可控。4.3 provide / inject跨层通信的正确打开方式当组件嵌套层级很深时一层一层传 props 和 emit 会非常痛苦代码里全是中间层组件“中转”数据。Vue 3 的 provide / inject 就是来解决这个问题的。父组件用 provide 提供数据任意深度的后代组件用 inject 接收不用关心中间有多少层。听起来很像状态管理工具但它没有全局的概念只对组件树上的“提供者”和“注入者”生效作用域更受控、更清晰。使用上有一个值得注意的细节provide 的值如果不是 ref 或 reactive那它是非响应式的。也就是说父组件提供的普通对象/字符串后面改了子组件并不会有感知。所以我一般会直接 provide 一个 ref 或 reactive 对象过去这样数据的更新能自然传递到注入端// 父组件 const globalConfig ref({ theme: dark }); provide(config, globalConfig); // 后代组件 const config inject(config);配合 readonly 可以进一步防止子组件意外修改注入数据从设计上养成单向数据流的习惯。这个思路在小型组件库里特别实用比引入 Pinia 要轻量得多。5. 动态组件与异步组件按需加载的关键手段5.1 用 component 标签实现动态切换很多管理后台的页面结构都是“一个容器 多个可切换的子组件”比如 Tab 切换、步骤条、弹窗内多步骤表单。如果用 v-if 写一堆分支代码很啰嗦而且子组件的状态管理很麻烦。Vue 内建的component组件可以完美解决这个需求它接收一个:is属性动态绑定要渲染的组件component :iscurrentTab :datapayload changeonChange /is的值可以是组件对象也可以是一个注册过的组件名。这种方式让组件切换变成纯数据驱动比如 currentTab 改一下界面就自动切换。配合 keep-alive 包裹还能保留各个组件的状态避免切换时组件被销毁重建keep-alive component :iscurrentTab / /keep-alive这里要注意区分动态组件不是异步组件它只是“运行时决定渲染哪个组件”的机制真正需要按需加载时还要结合下一节的异步组件特性。5.2 defineAsyncComponent 与代码分包在一个大型项目里把所有组件统统打进一个 bundle 是非常不明智的首屏体积会暴增加载时间肉眼可见地变长。Vue 3 的 defineAsyncComponent 可以配合 import 函数实现组件级代码分割const UserProfile defineAsyncComponent(() import(./components/UserProfile.vue) );这样 UserProfile 的代码会被单独打包成一个 chunk只在它确实要渲染时才去下载。在管理系统里像编辑弹窗、详情抽屉、图表大屏这类不进入首屏的组件用这种方式处理收益非常明显。实测下来首屏体积能减少 30% 以上前提是你把静态导入确实需要首屏渲染的资源排除在外。异步组件还有一个很容易被忽略的细节异步加载期间页面会出现空白或闪烁。如果异步加载长时间没完成更好的做法是提供 loading 和 error 状态defineAsyncComponent 也支持这些配置项。我给很多同事说的一句话是异步组件不是“用完就忘”的性能优化技巧它配置项里的 loadingComponent、delay、timeout 都是生产环境必须考虑的因素。5.3 场景联动动态组件 异步加载 生命周期动态组件和异步组件不是彼此独立的它们经常一起出现在复杂场景里。比如一个表单页面根据用户选择的表单类型动态加载对应的表单组件并传入不同的数据。在这种场景下组件的生命周期会随切换而触发 onUnmounted / onBeforeUnmount状态会被重置所以如果你希望切换到某个页面时临时保留数据就得靠 keep-alive 或者在父组件里缓存数据否则每次切回来都是“全新状态”。还有一点经验是动态组件上绑定的事件和 props 跟普通组件是完全一样的所以你可以把动态组件当作一个“普通组件变量”来理解——它只是“谁来渲染”不确定渲染出来的组件该有的生命周期、通信方式、响应式逻辑统统照旧。想清楚这个动态组件用起来会很顺手。6. 常见问题与排查技巧实录6.1 ref 在模板里到底要不要 .value不少从 Vue 2 转过来的同学刚用 ref 时容易陷入混乱JS 里写 ref.value模板里又直接写名字一会儿有点、一会儿没点特别容易出低级错误。记住一条铁律模板里不需要 .valueVue 会自动解包setup 里的 JS 代码中读写 ref 都要加 .value。还有一个容易忽略的例外在模板里如果你把 ref 对象作为函数的参数传入需要写 ref.value因为函数接收的是值不是响应式引用。比如button clickhandleClick(count)点击/button这里 handleClick 收到的是 count 的值如果你想把 count 这个“引用”传进去让别人修改就需要传 count 本身而不是 count.value。这两种用法语义完全不同写之前想清楚是要“读值”还是“传引用”。6.2 reactive 整体赋值丢失响应式这是我在实际项目中遇到最多的问题之一。很多人在设置一个对象类型数据时习惯直接整体赋值let state reactive({ list: [] }); // 请求回来之后直接覆盖 state { list: result };然后列表不刷新了页面还是空的。原因我之前提过reactive 返回的是原始对象的代理你对整个 state 变量重新赋值相当于把原来那个代理对象从变量上解绑了新对象并没有被代理当然不是响应式的。解决办法也很简单要么改成 ref 并用.value要么只修改对象的某个属性而不是整体覆盖要么通过Object.assign(state, newData)把新数据合并到原来的响应式对象中。习惯了这套规则之后就基本不会踩坑了。6.3 生命周期钩子“没执行”的排查方向很多人问为什么 onMounted 里的代码没执行。排查思路其实很明确先确认钩子是在 setup 内部直接同步调用注册的不要把它写在 setTimeout、Promise 回调或者某个方法里否则 Vue 拿不到正确的组件实例上下文注册会静默失败。另外一个常见原因是组件本身处于 keep-alive 里这种情况下首次进入触发的是 onMounted 和 onActivated但从别的页面切回来再次进入时只触发 onActivated不会重复触发 onMounted。很多人会困惑“为什么我的 onMounted 只跑了一次”其实 activate 没收到的钩子就去 onActivated / onDeactivated 里找这是 keep-alive 的设计使然。最后再提醒一个容易忽略的点异步组件的加载状态会直接影响生命周期触发顺序。如果你看到 onMounted 一直没执行检查一下父级是不是用了动态组件且组件还在 pending 加载中等组件真实挂载完成钩子自然会触发。6.4 组件销毁时定时器为什么还在跑这个问题的本质就是没清理资源。很多人的代码长这样onMounted(() { setInterval(() { loadData(); }, 5000); });然后页面上长期挂着这个组件定时器就一直执行即使组件已经销毁比如 v-if 切走了、路由跳走了定时器依然在跑因为循环引用的闭包还在。这时候要在 onBeforeUnmount 里 clearInterval。顺便说一个更隐蔽的坑如果你在 onUpdated 里注册了事件监听而这个监听器没有被移除它也会累积。用“注册即清理”的原则去写基本能避开 90% 的资源泄漏问题。我个人实际开发中觉得最顺手的组合是setup 里先声明数据再按功能块写逻辑初始化请求统一放 onMounteddialog 这类临时组件的资源都集中在 onBeforeUnmount 里处理。这套打法稳定跑了很久属于可以直接“抄作业”的模板。声明一下以上内容是基于 Vue 3 官方文档和我的实战经验整理的常见操作如果你用的是非规范写法比如直接操作 DOM 绕开响应式部分结论可能不适用但推荐的正常路径下这套体系完全够用。
返回列表