想象你正在升级家里的厨房。旧厨房里,食材按“类型”分柜收纳:所有蔬菜在一个柜子,所有调料在一个架子,所有锅具在另一个角落。表面井井有条,可每次做一道“鱼香肉丝”,你得在蔬菜柜、调料架、锅具角之间来回跑——拿肉(data数据)、找酱汁配方(methods方法)、确认火候(computed计算)、盯着锅别糊(watch侦听)。一道菜的完整逻辑被拆散在厨房的四个角落,改一下配料就要翻三个柜子。 Vue3的Composition API就是一次彻底的厨房改造:不再按“食材类型”收纳,而是按“菜谱”组织。setup函数像一张新备菜台,一道菜需要的备料、调味、火候、装盘全部在同一个操作台完成;ref和reactive是两种盛具,一个装单份调料的小碗,一个装整桌食材的大托盘;computed是自动切菜机,watch是定时闹钟,watchEffect是跟拍摄像头。想复用这道菜?直接把这整块操作台端走——这就是组合函数(Composables)。这篇文章会带你从备菜台开始,把Vue3这套新厨房的每个角落都走一遍。
对AI从业者的价值:AI前端项目(可视化大屏、Agent对话应用、训练任务管理台)场景复杂、状态繁多。掌握Composition API,你就能以“功能”为单元组织代码,让逻辑复用与协作效率显著提升。
目录
可视化演示
展示setup执行时机与context参数、ref与reactive响应式依赖追踪、Options API到Composition API迁移映射,步进切换探索。
本节配有交互动效(进入视口后自动加载,约 3KB)。
setup函数执行流程 setup在组件实例创建前执行,this为undefined 接收props参数和context含attrs/emit/slots setup(props, { attrs, emit, slots }) 返回值暴露给模板
setup函数基础
拿到一张新菜谱“红烧排骨”,第一步不是开火,而是在备菜台上备齐所有材料:排骨、葱姜、料酒、糖色。Vue3的setup函数就是这张备菜台——组件开始正式“烹饪”前,所有数据、方法、计算逻辑、侦听器都要先在这里码放整齐,等着被端上灶台。
在旧式Options API里,data写数据、methods写方法、computed写计算、watch写侦听,好比四个互相隔离的储物柜。setup打破了这种区隔:它把原本分散在各“选项”中的代码汇聚到一个函数里,函数执行后返回的对象直接暴露给模板使用。这种写法叫组合式API(Composition API),核心思想是“逻辑按功能聚拢”而不是“代码按类型分柜”。
setup的执行时机非常早——在组件实例初始化的最前端,早于beforeCreate和created。此时props已经解析完毕,它接收两个参数:props是父组件传入的属性集合,context则包含attrs、slots、emit三个成员。注意setup函数体内无法使用this,因为实例尚在初始化途中,这是很多Vue2老手踩的第一个坑。
所以,升级Vue3后打开一个组件,最先映入眼帘的不再是data而是setup——它像一张新备菜台,把所有用得到的“材料”先码整齐,后厨(渲染器)再开始点火炒菜。
- 接收 props 参数父组件声明的外部属性会以 props 对象形式传入 setup,在函数内只读使用,与选项式中的 props 含义一致。
- 接收 context 参数context 聚合了 attrs(非props属性)、slots(插槽内容)、emit(自定义事件发射器),是 setup 与外界通讯的通道。
- 返回对象绑定模板return 出去的对象会被展开到模板渲染上下文,模板中可直接书写其属性名,等价于旧 data 与 methods 的合并暴露。
ref响应式
厨房里最顺手的盛具是调料小碗:盐、糖、生抽各装一碗,单手可拿、随时替换、互不干扰。ref就是Vue3为“单个值”准备的这样一只小碗,尤其适合字符串、数字、布尔值这类基础数据。
用ref(0)就能创建一个响应式“小碗”。在JavaScript逻辑中,想读盐得先揭开碗盖——ref的读写都需要通过.value访问;但在模板里,Vue会自动帮你“揭开碗盖”,直接写count就能拿到值。这就像备菜时从碗里取盐,而炒菜时碗已经敞开放在灶台上。
有人以为ref只服务基础类型,其实ref内部遇到对象时,会把对象交给reactive的Proxy机制处理。所以ref({ list: [] })一样拥有深层响应能力。ref真正的独特优势是“支持整体替换”:count.value = newObject时,所有依赖自动收到新值,而reactive做不到这一点。
总结来说,ref是那只“单手可拿的小碗”:基础类型、组件状态、需要整体替换的场景,选用它准没错。
- 内部包装ref将值包装为带有value属性的响应式对象,基本类型因此也能被“追踪”。
- 读取收集依赖访问
.value时,当前正在运行的渲染函数或副作用会被登记为该数据的依赖,相当于“这碗盐被哪道菜用了”记进台账。 - 写入派发更新修改
.value时,台账上所有登记过的依赖被依次通知,触发组件重新渲染或副作用重跑。
reactive响应式
整只鸡、整条鱼、一篮子蔬菜,小碗装不下,得换一个大托盘。reactive就是那只大托盘——它接受一个对象或数组,返回一个深层响应的代理对象。你可以像从前操作普通对象一样操作它,而它会在背后默默完成依赖收集与更新派发。
reactive的底层是Proxy(代理机制)。当你访问state.user.name,get拦截器会逐层为嵌套对象建立代理;当你修改属性,set拦截器会触发更新。数据多、嵌套深、以“对象整体”为操作单位时,reactive能让代码比ref的.value链简洁得多。
但reactive有两个“烫手”特性:一是不能直接整体替换——state = newObj会让旧代理脱离响应轨道,新对象变成普通数据;二是解构会丢失响应性——const { name } = state取出的name不再被追踪。这两个坑分别对应思维导图中的“引用替换”和“解构丢失”。
大托盘适合“整套餐具”一起管理,但如果你要频繁“换一套餐具”,ref那只小碗更稳妥。选择ref还是reactive,核心就看一件事:是否需要整体替换引用。
- Proxy 代理对象reactive 用 Proxy 包装普通对象,拦截属性的读取、写入、删除等操作。
- get 收集依赖任何属性被读取时,当前上下文(渲染函数或副作用)会被登记为该属性的依赖。
- set 派发更新属性被改写时,触发该属性依赖列表中的全部订阅者重新执行。
computed计算属性
厨房里有台自动切菜机:土豆放进去,出来是土豆丝;换胡萝卜进去,出来是胡萝卜丝。你不需要手动切,机器自动跟着原料走。computed就是这台备菜机——它接收一个getter函数,根据响应式依赖自动计算派生值。
computed返回的是一个只读的ref对象,它有两个迷人特性:依赖自动追踪与缓存。getter里读取到的每一个响应式数据都会被登记为依赖;只要依赖没有变化,多次访问computed直接返回上一次计算结果,绝不多算一次。这好比切菜机已经切好了一碗土豆丝,只要土豆没换,你随时来拿都有现成的。
computed甚至支持“可写”版本:传入一个包含get和set的对象,就能在赋值时反向推导依赖数据的更新。而methods里的普通函数每次渲染都会重新执行,没有缓存。一句话区分:数据“派生”用computed,事件“动作”用methods。
当你需要“根据已有材料自动生成半成品”——比如列表筛选、总数统计、格式化文本,交给computed这台自动切菜机,又快又省心。
- getter 执行computed 创建时立即执行一次 getter,记录访问到的所有响应式依赖。
- 缓存标记只要 deps 中的任一数据未改变,后续访问返回值直接取自缓存,不重跑 getter。
- 失效重算任一依赖改变时,computed 标记为“脏”,下次访问重新执行 getter 并刷新缓存。
watch侦听器
煲汤时你会设一个闹钟:水开了关小火,起锅前撒葱花。watch就是这只闹钟——你明确告诉它“盯哪口锅、什么时候响、响了做什么”,它平时绝不打扰你。
在Composition API中,watch(source, callback, options)的三个部分都很直观:source是要盯的数据源,可以是ref、reactive对象、getter函数或它们的数组;callback接收(newVal, oldVal)两个参数,让你知道“汤”从什么状态变成了什么状态。它默认惰性——source真正变化时才执行,正符合“闹钟平时不响”的直觉。
watch还支持丰富的配置:deep: true递归侦听对象内部的深层变化,immediate: true让回调在创建时立即执行一次,适合“初始化时也要跑一遍逻辑”的场景。此外,watch回调内可以返回一个清理函数,用于取消旧请求、清除定时器,避免上一个任务污染下一个任务。
watch像一只精准的闹钟,最适合“某数据变化后做特定动作”——比如筛选条件变了重新请求数据、用户登录状态变了跳转页面。
- 指定数据源第一个参数 source 指定要侦听的响应式数据,支持 ref、reactive 对象、getter 函数。
- 定义回调第二个参数 callback 在数据变化时执行,接收新旧两个值用于比对与联动。
- 配置选项第三个参数 options 控制深度侦听与立即执行。
watchEffect侦听器
如果watch是闹钟,watchEffect就像一个永远跟拍你的助理摄像机:你走到哪它拍到哪,看到你动作变了就立刻跟上,不需要你提前告诉它“重点盯什么”。
watchEffect(fn)会立即执行一次fn,执行过程中读取到的所有响应式数据都被自动登记为依赖;之后任何一个依赖发生变化,fn又会自动重跑。它天然是“收集式”的——依赖不是声明出来的,而是“用过即追踪”。这正适合日志上报、埋点统计这类“读了一堆数据,任一变化都要重跑”的场景。
与watch对比:watch必须显式声明source、惰性执行、回调能拿到旧值;watchEffect不需要声明数据源、立即执行、拿不到旧值,但写法极简。要手动停止侦听时,watchEffect会返回一个停止句柄,调用它即可。组合函数内部使用watchEffect尤其顺手,因为它零配置、随用随走。
摄像头不需要你告诉它重点——当逻辑里“读了一堆数据,任何一处的变化都要重跑”时,用watchEffect就是最省心的选择。
- 立即执行调用 watchEffect 后,回调 eff 会马上运行一次,完成首次计算。
- 自动收集依赖eff 运行期间每次访问响应式属性,该属性都会把 eff 登记为依赖。
- 自动重跑任一被收集的依赖变化时,eff 自动再次执行,无需手动声明侦听目标。
生命周期变化
做一道菜有固定流程:备菜、下锅、加水、收汁、装盘。Vue组件的生命也类似——创建、挂载、更新、卸载。Vue3在保留这套流程的同时,把钩子从“选项”改成了“函数”。
最明显的变化是:beforeCreate和created被setup本身取代了,因为setup的执行时机就落在创建阶段。其余钩子统一改为onXxx函数形式,并且必须在setup内调用:mounted→onMounted、updated→onUpdated、beforeUnmount→onBeforeUnmount、unmounted→onUnmounted。此外,Vue2的beforeDestroy和destroyed在Vue3中更名为onBeforeUnmount和onUnmounted。
组合式钩子有一个旧选项式没有的优势:可以多次调用。同一个onMounted可以按代码顺序注册多个回调,这让“同一个功能的初始化代码”可以写在一起,而不是分散在各处。比如“启动轮询”和“绑定事件”两段初始化,在逻辑上属于同一功能时,就能在同一个组合函数里相邻注册。
就像菜谱把“预热烤箱”和“准备模具”合并为一句“准备开始”,setup让创建阶段有了统一入口,但烹饪的关键时间节点一个都没少。
- 创建期合并beforeCreate 与 created 两个旧选项被 setup 取代,创建阶段的逻辑统一放入 setup。
- 挂载钩子改名mounted 改为 onMounted,作为函数在 setup 内调用以注册回调。
- 卸载钩子改名beforeDestroy/destroyed 统一更名为 onBeforeUnmount/onUnmounted,语义更贴近“卸载”阶段。
迁移与取舍
老厨房升级不必推倒重来,而是逐个灶台改造。从Options迁移到Composition同样如此:不需要一次性重写整个项目,可以一页一页、一个功能一个功能地搬。
推荐的迁移路径:第一步,引入<script setup>语法糖,让组合式写法更简洁;第二步,把data里的数据替换为ref/reactive;第三步,methods里的函数改为setup内的普通函数;第四步,computed和watch照搬,生命周期钩子改名;最后,观察哪些逻辑与组件实例无关,抽取为useXxx组合函数。useLogin、useChart、usePermissions这样的函数,就是那块可以“整盘端走”的操作台。
组合函数(Composables)是Composition API的灵魂:它把数据、计算、侦听、生命周期全部封装在一个函数里,跨组件复用如同一键复制菜谱。但同时也要提醒一句“保持简单”——简单的展示型页面继续用Options API完全没问题,不必为改造而改造。迁移的衡量标准只有一个:代码是否更容易理解和维护。
厨房改造的终点不是“用上所有新工具”,而是“每道菜都有清晰的流程”。用Composition去聚合真正重复、混乱的逻辑,才是它最大的价值。
- 逐步提取从单个功能出发,把该功能在 data/methods/computed/watch 中的片段集中搬出。
- 封装组合函数相关逻辑打包为 useXxx 函数,函数内部统一管理状态与副作用。
- 按需调用组件 setup 中调用 useXxx(),通过解构获取需要的状态与方法。
AI 应用场景
场景一:AI可视化大屏的实时数据管道
阶段:前端状态管理与副作用编排
解决的问题:AI缺陷检测大屏需要同时处理WebSocket推送、模型推理进度、图表实时刷新。传统Options写法中,socket连接逻辑在created、消息处理散落在methods、轮询定时器挂在watch,改一个数据流要跨三个区域修改,极易漏改。用Composition API把“数据采集-状态更新-图表联动”打包成useLiveData(url)组合函数,一块操作台管完整个管道。
具体案例:某工厂流水线质检大屏,原本由三个组件共享一份socket数据,每次修改连接逻辑至少改动4个文件。重构后,useLiveData被三个组件同时调用,连接、消息解析、自动重连、错误上报全部收敛在一个函数里,两周迭代周期缩短到三天。
场景二:AI Agent对话组件库开发
阶段:复杂交互状态机管理
解决的问题:面向大模型的多轮对话组件有大量状态:输入内容、消息列表、流式输出缓存、停止标志。这些状态分散在多个组件时,共享与同步非常痛苦。使用useChatSession组合函数,把消息流、生成状态、错误重试封装在一起,任何页面只需调用一次即可获得完整会话能力。
具体案例:某AI客服产品需要在官网、后台、移动端三处复用同一套对话交互。团队用useChatSession统一管理会话状态,通过computed派生消息列表、watch处理流式输出的增量追加,三端共用一套逻辑,新页面接入只需三行代码。
场景三:AI训练任务管理台
阶段:训练任务的进度与状态管理
解决的问题:训练任务管理台有大量重复逻辑:任务进度、日志流、状态轮询。每个任务卡片都重复实现这些逻辑。用useTaskTracker组合函数统一封装,reactive管理任务对象,computed计算平均进度,watchEffect根据状态自动切换轮询与停止。
具体案例:某算法团队的管理后台有上千个训练任务卡片,旧代码中每个卡片组件都维护着自己的一套定时器。重构为useTaskTracker后,任务状态派生(如“是否可取消”)由computed自动计算,状态流转由watchEffect侦听,页面整体代码量减少约40%。
Python 演示
"""用 Python 模拟 Vue3 的 ref / computed / watchEffect 响应式原理"""class Ref: """模拟 ref:包装单值,读取收集依赖,写入派发更新""" _active = None # 当前正在运行的副作用(effect) def __init__(self, value): self._value = value self._effects = set() @property def value(self): if Ref._active is not None: self._effects.add(Ref._active) return self._value @value.setter def value(self, new_value): if new_value != self._value: self._value = new_value for fn in list(self._effects): fn() def watch_effect(fn): """模拟 watchEffect:立即执行并自动收集依赖""" Ref._active = fn fn() Ref._active = None def computed(fn): """模拟 computed:用 Ref 作为缓存盒,依赖变化时自动重算""" box = Ref(None) def recompute(): box.value = fn() watch_effect(recompute) return box if __name__ == "__main__": count = Ref(1) doubled = computed(lambda: count.value * 2) logs = [] watch_effect(lambda: logs.append(f"doubled={doubled.value}")) count.value = 3 count.value = 5 print(" -> ".join(logs)) # 输出: doubled=2 -> doubled=6 -> doubled=10
Ref._active = None类变量作为“当前正在运行的副作用指针”,是依赖收集的关键。@property def value用property让count.value读起来像普通属性,getter中完成依赖登记。self._effects.add(Ref._active)读取时把正在运行的effect登记为订阅者,这就是“收集依赖”。for fn in list(self._effects)写入时对订阅者集合做快照遍历,逐个通知更新。watch_effect(fn)先把fn设为全局活跃effect再执行它,让首次运行的读取操作自动完成收集。box.value = fn()computed中的recompute将计算结果写入缓存盒,触发依赖这个盒子的effect重新执行。
常见误区
误区我听说ref是给字符串、数字用的,对象和数组一定要用reactive,所以我把所有对象都改成了reactive写法。
正解ref内部遇到对象时会自动交给reactive的Proxy机制处理,ref({ list: [] })同样深层响应。选择依据不是“类型”而是“是否需要整体替换”:如果代码中常有xxx.value = newData这种整体赋值的写法,ref更合适;如果只是修改内部属性,reactive更简洁。小碗也能装大菜,只是端法不同。
为何常见:很多教程为了对比清晰,刻意强调“ref管基础类型、reactive管对象”,导致初学者把“建议用法”理解成了“硬性规则”。
误区我用`const { name, age } = state`从reactive对象里解构出属性,以为它们和Vue2的data解构一样好用,结果页面不更新了。
正解解构的本质是取出“当前值”,脱离了Proxy代理的对象,读取与赋值都不再经过get/set拦截,自然丢失响应。需要解构并保持响应时,应使用toRefs(state)将每个属性转为ref再解构,或干脆不解构,直接state.name访问。托盘里的菜夹到小碟子,就不再受托盘监测。
为何常见:ES6解构赋值太日常了,开发者下意识地对reactive对象使用,完全没有意识到Proxy代理的拦截范围。
误区既然都是“数据变了执行回调”,那watchEffect写法更短,我全部用它不就行了?
正解两者定位完全不同。watch需要显式指定数据源、惰性执行、回调能拿到新旧值,适合“某个数据变化后做特定动作”;watchEffect自动收集所有读取的依赖、立即执行、拿不到旧值,适合“任一依赖变化全量重跑”。若在watchEffect里调用异步接口并依赖异步结果,很容易因依赖收集不完整而漏触发,此时watch反而更可控。
为何常见:两者名字相似、功能重叠,教程若不强调“显式vs自动”“惰性vs立即”的差异,初学者很难区分适用边界。
误区setup取代了data、methods、computed、watch,那我干脆把所有逻辑全部写进setup函数里,一个文件几百行也没关系。
正解setup只是“备菜台入口”,不是“无限储物间”。最佳实践是把关联逻辑提取成useXxx组合函数,setup里只保留调用与编排。一个几百行的setup与一个几百行的Options组件一样难以维护——菜谱的要点不是“全部堆在台面上”,而是“按菜分装、随取随用”。
为何常见:初学组合式API时,发现所有选项都能写进setup,误以为“能写”等于“该写”,忽略了逻辑聚合与复用才是Composition的设计初衷。
面试题
Q1. setup 函数在组件生命周期中的执行时机是?
- A.beforeCreate 之前执行← 正确答案
- B.created 之后才执行
- C.mounted 之后才执行
- D.首次渲染完成后执行
答案:A。setup在组件实例初始化最早期执行,早于beforeCreate和created。它替代了这两个旧选项的职责,所以实例尚未挂载、this无法使用。B、C、D都发生在setup之后。
Q2. 在 script 中读取 ref 创建的值,正确的方式是?
- A.直接书写 count 即可
- B.必须写 count.value← 正确答案
- C.调用 count() 函数
- D.用 this.count 访问
答案:B。ref创建的是包含value属性的对象,脚本中读写都要通过.value。模板中会自动解包可直接写count,但这是模板编译器的特殊处理;this.count因setup中无this而不可用。
Q3. 关于 ref 与 reactive,下列说法正确的有?
- A.reactive 可以整体替换整个对象
- B.ref 适合基础与整体替换场景← 正确答案
- C.reactive 底层基于 Proxy 代理← 正确答案
- D.ref 模板中必须写 .value 访问
答案:BC。reactive不能整体替换,否则新对象脱离代理,A错。ref适合基础类型和需要xxx.value = newData整体替换的场景,B对。reactive基于Proxy实现深层代理,C对。模板中ref自动解包,无需写.value,D错。
Q4. 对 reactive 对象进行解构后,取出的属性会?
- A.保持原有深层响应性
- B.丢失响应特性← 正确答案
- C.自动转换成为 ref
- D.变成普通函数
答案:B。解构取出的是当前值,脱离了Proxy代理,后续读取与赋值不再被拦截,响应性丢失。需要保持响应时用toRefs转换。自动转ref是不存在的,解构也不会变成函数。
Q5. computed 区别于普通方法的核心特性是?
- A.每次访问都重新计算
- B.依赖不变时返回缓存← 正确答案
- C.能自动发起网络请求
- D.只能返回布尔类型
答案:B。computed会追踪依赖并缓存结果,依赖不变时直接返回缓存,这是与普通方法最本质的区别。每次重算是method的特性;computed不应有网络请求副作用;返回类型任意。
Q6. watch 与 watchEffect 的核心区别是?
- A.watch 立即执行且惰性
- B.watch 需显式指定数据源← 正确答案
- C.watchEffect 能拿到新旧值
- D.watchEffect 是非响应式的
答案:B。watch必须显式声明source且默认惰性执行;watchEffect自动收集依赖并立即执行、拿不到旧值但写法简洁。A中“立即执行”描述错误,C、D与事实相反。
Q7. 以下场景更适合使用 watchEffect 的是?
- A.需要在变化时访问旧值
- B.多个依赖任一变都要重跑← 正确答案
- C.想省去显式声明数据源← 正确答案
- D.需要精确控制侦听对象
答案:BC。watchEffect自动收集所有读取的依赖,任一变化都重跑且拿不到旧值,所以B、C适合。需要旧值(A)或精确控制侦听对象(D)时,应使用watch。
Q8. 无法在 setup 中使用 this 的原因是?
- A.组件实例尚未被创建
- B.早期执行 this 未绑定← 正确答案
- C.模板不再需要该实例
- D.Vue3 中已废弃实例
答案:B。setup在实例初始化早期执行,此时实例尚未完成装配,this无法正确绑定到组件实例。组件实例本身依然存在,只是setup阶段无法通过this访问;A的说法不准确,C和D则与事实不符。
Q9. 下列属于 setup 合法参数的是?
- A.props 和 context← 正确答案
- B.this 能访问组件实例
- C.slots 来自 context← 正确答案
- D.emit 来自 context← 正确答案
答案:ACD。setup接收props与context两个参数,context内含attrs、slots、emit,所以A、C、D正确。setup中无法通过this访问组件实例,B错误。
Q10. 在 watchEffect 回调里异步读取响应式数据,会?
- A.同步与异步都会收集
- B.只收集同步部分的依赖← 正确答案
- C.不收集任何响应依赖
- D.会收集全部已定义数据
答案:B。watchEffect只在回调同步执行期间收集依赖。异步回调执行时,Ref._active已经清空,此时读取的数据不会被登记,因此依赖变化时不会触发重跑。这是“依赖收集不完整”的经典陷阱,复杂异步逻辑应改用watch显式声明数据源。