想象你正在升级家里的厨房。旧厨房里,食材按“类型”分柜收纳:所有蔬菜在一个柜子,所有调料在一个架子,所有锅具在另一个角落。表面井井有条,可每次做一道“鱼香肉丝”,你得在蔬菜柜、调料架、锅具角之间来回跑——拿肉(data数据)、找酱汁配方(methods方法)、确认火候(computed计算)、盯着锅别糊(watch侦听)。一道菜的完整逻辑被拆散在厨房的四个角落,改一下配料就要翻三个柜子。 Vue3的Composition API就是一次彻底的厨房改造:不再按“食材类型”收纳,而是按“菜谱”组织。setup函数像一张新备菜台,一道菜需要的备料、调味、火候、装盘全部在同一个操作台完成;ref和reactive是两种盛具,一个装单份调料的小碗,一个装整桌食材的大托盘;computed是自动切菜机,watch是定时闹钟,watchEffect是跟拍摄像头。想复用这道菜?直接把这整块操作台端走——这就是组合函数(Composables)。这篇文章会带你从备菜台开始,把Vue3这套新厨房的每个角落都走一遍。

岗位关联

对AI从业者的价值:AI前端项目(可视化大屏、Agent对话应用、训练任务管理台)场景复杂、状态繁多。掌握Composition API,你就能以“功能”为单元组织代码,让逻辑复用与协作效率显著提升。

目录
  1. 可视化演示
  2. setup函数基础
  3. ref响应式
  4. reactive响应式
  5. computed计算属性
  6. watch侦听器
  7. watchEffect侦听器
  8. 生命周期变化
  9. 迁移与取舍
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

展示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——它像一张新备菜台,把所有用得到的“材料”先码整齐,后厨(渲染器)再开始点火炒菜。

setup(props, context)⟶返回值→ 绑定 模板渲染\displaystyle \text{setup}(\text{props},\ \text{context}) \longrightarrow \text{返回值} \xrightarrow{\ \text{绑定}\ } \text{模板渲染}
备菜台前的两样工具:props是客人递来的点菜单(外部属性),context是通往后厨的传菜铃(插槽、事件、非prop属性)。
  1. 接收 props 参数父组件声明的外部属性会以 props 对象形式传入 setup,在函数内只读使用,与选项式中的 props 含义一致。
    props={父组件传入的属性名:值}\displaystyle \text{props} = \{\text{父组件传入的属性名}: \text{值}\}
  2. 接收 context 参数context 聚合了 attrs(非props属性)、slots(插槽内容)、emit(自定义事件发射器),是 setup 与外界通讯的通道。
    context={attrs, slots, emit}\displaystyle \text{context} = \{\text{attrs},\ \text{slots},\ \text{emit}\}
  3. 返回对象绑定模板return 出去的对象会被展开到模板渲染上下文,模板中可直接书写其属性名,等价于旧 data 与 methods 的合并暴露。
    setup 的返回值→模板可访问的响应式状态与方法\displaystyle \text{setup 的返回值} \to \text{模板可访问的响应式状态与方法}

ref响应式

厨房里最顺手的盛具是调料小碗:盐、糖、生抽各装一碗,单手可拿、随时替换、互不干扰。ref就是Vue3为“单个值”准备的这样一只小碗,尤其适合字符串、数字、布尔值这类基础数据。

用ref(0)就能创建一个响应式“小碗”。在JavaScript逻辑中,想读盐得先揭开碗盖——ref的读写都需要通过.value访问;但在模板里,Vue会自动帮你“揭开碗盖”,直接写count就能拿到值。这就像备菜时从碗里取盐,而炒菜时碗已经敞开放在灶台上。

有人以为ref只服务基础类型,其实ref内部遇到对象时,会把对象交给reactive的Proxy机制处理。所以ref({ list: [] })一样拥有深层响应能力。ref真正的独特优势是“支持整体替换”:count.value = newObject时,所有依赖自动收到新值,而reactive做不到这一点。

总结来说,ref是那只“单手可拿的小碗”:基础类型、组件状态、需要整体替换的场景,选用它准没错。

ref(v)={ value:v,读取→收集依赖,写入→通知更新 }\displaystyle \text{ref}(v) = \{\, \text{value}: v,\quad \text{读取} \to \text{收集依赖},\quad \text{写入} \to \text{通知更新} \,\}
ref像是带盖的小碗:在脚本里取用要揭盖(.value),在模板中碗盖已揭开,直接拿。
  1. 内部包装ref将值包装为带有value属性的响应式对象,基本类型因此也能被“追踪”。
    ref(0)={value:0}\displaystyle \text{ref}(0) = \{\text{value}: 0\}
  2. 读取收集依赖访问.value时,当前正在运行的渲染函数或副作用会被登记为该数据的依赖,相当于“这碗盐被哪道菜用了”记进台账。
    count.value⇒登记当前上下文为依赖\displaystyle \text{count.value} \Rightarrow \text{登记当前上下文为依赖}
  3. 写入派发更新修改.value时,台账上所有登记过的依赖被依次通知,触发组件重新渲染或副作用重跑。
    count.value=5⇒∀ 依赖:重新执行\displaystyle \text{count.value} = 5 \Rightarrow \forall\ \text{依赖}: \text{重新执行}

reactive响应式

整只鸡、整条鱼、一篮子蔬菜,小碗装不下,得换一个大托盘。reactive就是那只大托盘——它接受一个对象或数组,返回一个深层响应的代理对象。你可以像从前操作普通对象一样操作它,而它会在背后默默完成依赖收集与更新派发。

reactive的底层是Proxy(代理机制)。当你访问state.user.name,get拦截器会逐层为嵌套对象建立代理;当你修改属性,set拦截器会触发更新。数据多、嵌套深、以“对象整体”为操作单位时,reactive能让代码比ref的.value链简洁得多。

但reactive有两个“烫手”特性:一是不能直接整体替换——state = newObj会让旧代理脱离响应轨道,新对象变成普通数据;二是解构会丢失响应性——const { name } = state取出的name不再被追踪。这两个坑分别对应思维导图中的“引用替换”和“解构丢失”。

大托盘适合“整套餐具”一起管理,但如果你要频繁“换一套餐具”,ref那只小碗更稳妥。选择ref还是reactive,核心就看一件事:是否需要整体替换引用。

reactive(obj)=Proxy⏟get 收集依赖 / set 派发更新(obj, handlers)\displaystyle \text{reactive}(\text{obj}) = \underbrace{\text{Proxy}}_{\text{get 收集依赖 / set 派发更新}}(\text{obj},\ \text{handlers})
reactive像整桌食材的大托盘:整体端走很顺手,但把菜夹到小碟子里(解构),那碟菜就不再受托盘监视了。
  1. Proxy 代理对象reactive 用 Proxy 包装普通对象,拦截属性的读取、写入、删除等操作。
    reactive(O)=Proxy(O, get/set/deleteProperty)\displaystyle \text{reactive}(O) = \text{Proxy}(O,\ \text{get/set/deleteProperty})
  2. get 收集依赖任何属性被读取时,当前上下文(渲染函数或副作用)会被登记为该属性的依赖。
    O.p 读取⇒deps(O.p)∪{当前上下文}\displaystyle O.p \ \text{读取} \Rightarrow \text{deps}(O.p) \cup \{ \text{当前上下文} \}
  3. set 派发更新属性被改写时,触发该属性依赖列表中的全部订阅者重新执行。
    O.p=v⇒deps(O.p).forEach(fn→fn())\displaystyle O.p = v \Rightarrow \text{deps}(O.p).\text{forEach}(fn \to fn())

computed计算属性

厨房里有台自动切菜机:土豆放进去,出来是土豆丝;换胡萝卜进去,出来是胡萝卜丝。你不需要手动切,机器自动跟着原料走。computed就是这台备菜机——它接收一个getter函数,根据响应式依赖自动计算派生值。

computed返回的是一个只读的ref对象,它有两个迷人特性:依赖自动追踪与缓存。getter里读取到的每一个响应式数据都会被登记为依赖;只要依赖没有变化,多次访问computed直接返回上一次计算结果,绝不多算一次。这好比切菜机已经切好了一碗土豆丝,只要土豆没换,你随时来拿都有现成的。

computed甚至支持“可写”版本:传入一个包含get和set的对象,就能在赋值时反向推导依赖数据的更新。而methods里的普通函数每次渲染都会重新执行,没有缓存。一句话区分:数据“派生”用computed,事件“动作”用methods。

当你需要“根据已有材料自动生成半成品”——比如列表筛选、总数统计、格式化文本,交给computed这台自动切菜机,又快又省心。

computed(f)={依赖变化⇒重新执行 f依赖未变⇒返回缓存结果\displaystyle \text{computed}(f) =\begin{cases}\text{依赖变化} \Rightarrow \text{重新执行}\ f \\\text{依赖未变} \Rightarrow \text{返回缓存结果}\end{cases}
computed像自动切菜机:原料(依赖)没换,就直接从出货口拿上次切好的丝;原料一换,机器自动重启。
  1. getter 执行computed 创建时立即执行一次 getter,记录访问到的所有响应式依赖。
    f:deps(f)={getter 中读取的响应式数据}\displaystyle f: \text{deps}(f) = \{\text{getter 中读取的响应式数据}\}
  2. 缓存标记只要 deps 中的任一数据未改变,后续访问返回值直接取自缓存,不重跑 getter。
    deps 未变⇒result=cache\displaystyle \text{deps 未变} \Rightarrow \text{result} = \text{cache}
  3. 失效重算任一依赖改变时,computed 标记为“脏”,下次访问重新执行 getter 并刷新缓存。
    ∃d∈deps:d 变化⇒cache=f()\displaystyle \exists d \in \text{deps}: d\ \text{变化} \Rightarrow \text{cache} = f()

watch侦听器

煲汤时你会设一个闹钟:水开了关小火,起锅前撒葱花。watch就是这只闹钟——你明确告诉它“盯哪口锅、什么时候响、响了做什么”,它平时绝不打扰你。

在Composition API中,watch(source, callback, options)的三个部分都很直观:source是要盯的数据源,可以是ref、reactive对象、getter函数或它们的数组;callback接收(newVal, oldVal)两个参数,让你知道“汤”从什么状态变成了什么状态。它默认惰性——source真正变化时才执行,正符合“闹钟平时不响”的直觉。

watch还支持丰富的配置:deep: true递归侦听对象内部的深层变化,immediate: true让回调在创建时立即执行一次,适合“初始化时也要跑一遍逻辑”的场景。此外,watch回调内可以返回一个清理函数,用于取消旧请求、清除定时器,避免上一个任务污染下一个任务。

watch像一只精准的闹钟,最适合“某数据变化后做特定动作”——比如筛选条件变了重新请求数据、用户登录状态变了跳转页面。

watch(source, cb)=source 变化⇒cb(newVal, oldVal)\displaystyle \text{watch}(\text{source},\ \text{cb}) = \text{source 变化} \Rightarrow \text{cb}(\text{newVal},\ \text{oldVal})
watch像烹饪闹钟:你指定盯哪口锅,水开(数据变化)才响,响的时候还告诉你之前和现在的状态。
  1. 指定数据源第一个参数 source 指定要侦听的响应式数据,支持 ref、reactive 对象、getter 函数。
    source∈{ref, reactive, getter:()→值}\displaystyle \text{source} \in \{\text{ref},\ \text{reactive},\ \text{getter}: () \to \text{值}\}
  2. 定义回调第二个参数 callback 在数据变化时执行,接收新旧两个值用于比对与联动。
    cb:(newVal, oldVal)→业务逻辑\displaystyle \text{cb}: (\text{newVal},\ \text{oldVal}) \to \text{业务逻辑}
  3. 配置选项第三个参数 options 控制深度侦听与立即执行。
    options={deep:bool, immediate:bool}\displaystyle \text{options} = \{ \text{deep}: \text{bool},\ \text{immediate}: \text{bool} \}

watchEffect侦听器

如果watch是闹钟,watchEffect就像一个永远跟拍你的助理摄像机:你走到哪它拍到哪,看到你动作变了就立刻跟上,不需要你提前告诉它“重点盯什么”。

watchEffect(fn)会立即执行一次fn,执行过程中读取到的所有响应式数据都被自动登记为依赖;之后任何一个依赖发生变化,fn又会自动重跑。它天然是“收集式”的——依赖不是声明出来的,而是“用过即追踪”。这正适合日志上报、埋点统计这类“读了一堆数据,任一变化都要重跑”的场景。

与watch对比:watch必须显式声明source、惰性执行、回调能拿到旧值;watchEffect不需要声明数据源、立即执行、拿不到旧值,但写法极简。要手动停止侦听时,watchEffect会返回一个停止句柄,调用它即可。组合函数内部使用watchEffect尤其顺手,因为它零配置、随用随走。

摄像头不需要你告诉它重点——当逻辑里“读了一堆数据,任何一处的变化都要重跑”时,用watchEffect就是最省心的选择。

watchEffect(eff)=立即执行 eff⏟运行中自动收集依赖+依赖变化时重跑 eff⏟无需声明 source\displaystyle \text{watchEffect}(\text{eff}) = \underbrace{\text{立即执行 eff}}_{\text{运行中自动收集依赖}} + \underbrace{\text{依赖变化时重跑 eff}}_{\text{无需声明 source}}
watchEffect像跟拍摄像头:不用告诉它拍谁,它自己看你在读什么数据,你一变它就自动重新录制。
  1. 立即执行调用 watchEffect 后,回调 eff 会马上运行一次,完成首次计算。
    watchEffect(eff)⇒eff() 立即执行\displaystyle \text{watchEffect}(\text{eff}) \Rightarrow \text{eff}()\ \text{立即执行}
  2. 自动收集依赖eff 运行期间每次访问响应式属性,该属性都会把 eff 登记为依赖。
    ∀x∈eff 访问的属性:deps(x)∪{eff}\displaystyle \forall x \in \text{eff 访问的属性}: \text{deps}(x) \cup \{\text{eff}\}
  3. 自动重跑任一被收集的依赖变化时,eff 自动再次执行,无需手动声明侦听目标。
    ∃x∈deps:x 变化⇒eff()\displaystyle \exists x \in \text{deps}: x\ \text{变化} \Rightarrow \text{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⟶setupcreated⟶setupmounted⟶onMountedunmounted⟶onUnmounted\displaystyle \begin{aligned}\text{beforeCreate} & \longrightarrow \text{setup} \\\text{created} & \longrightarrow \text{setup} \\\text{mounted} & \longrightarrow \text{onMounted} \\\text{unmounted} & \longrightarrow \text{onUnmounted}\end{aligned}
原来“预热烤箱”和“准备模具”是两个独立提醒,现在合并成了一句话“准备开始”;关键节点一个不少,只是叫法更统一。
  1. 创建期合并beforeCreate 与 created 两个旧选项被 setup 取代,创建阶段的逻辑统一放入 setup。
    {beforeCreate, created}⇒setup()\displaystyle \{\text{beforeCreate},\ \text{created}\} \Rightarrow \text{setup}()
  2. 挂载钩子改名mounted 改为 onMounted,作为函数在 setup 内调用以注册回调。
    mounted⇒onMounted(cb)\displaystyle \text{mounted} \Rightarrow \text{onMounted}(\text{cb})
  3. 卸载钩子改名beforeDestroy/destroyed 统一更名为 onBeforeUnmount/onUnmounted,语义更贴近“卸载”阶段。
    beforeDestroy⇒onBeforeUnmount,destroyed⇒onUnmounted\displaystyle \text{beforeDestroy} \Rightarrow \text{onBeforeUnmount},\quad \text{destroyed} \Rightarrow \text{onUnmounted}

迁移与取舍

老厨房升级不必推倒重来,而是逐个灶台改造。从Options迁移到Composition同样如此:不需要一次性重写整个项目,可以一页一页、一个功能一个功能地搬。

推荐的迁移路径:第一步,引入<script setup>语法糖,让组合式写法更简洁;第二步,把data里的数据替换为ref/reactive;第三步,methods里的函数改为setup内的普通函数;第四步,computed和watch照搬,生命周期钩子改名;最后,观察哪些逻辑与组件实例无关,抽取为useXxx组合函数。useLogin、useChart、usePermissions这样的函数,就是那块可以“整盘端走”的操作台。

组合函数(Composables)是Composition API的灵魂:它把数据、计算、侦听、生命周期全部封装在一个函数里,跨组件复用如同一键复制菜谱。但同时也要提醒一句“保持简单”——简单的展示型页面继续用Options API完全没问题,不必为改造而改造。迁移的衡量标准只有一个:代码是否更容易理解和维护。

厨房改造的终点不是“用上所有新工具”,而是“每道菜都有清晰的流程”。用Composition去聚合真正重复、混乱的逻辑,才是它最大的价值。

Options→ 逐步提取 useXxx()→ 按需调用 逻辑聚合复用\displaystyle \text{Options} \xrightarrow{\ \text{逐步提取}\ } \text{useXxx}() \xrightarrow{\ \text{按需调用}\ } \text{逻辑聚合复用}
改造厨房的目标不是“看起来高级”,而是“做菜更顺手”。能用一块操作台搞定的菜谱,绝不拆成四个柜子。
  1. 逐步提取从单个功能出发,把该功能在 data/methods/computed/watch 中的片段集中搬出。
    功能片段:{data ∪ methods ∪ computed ∪ watch}⇒组合函数体\displaystyle \text{功能片段}: \{\text{data}\ \cup\ \text{methods}\ \cup\ \text{computed}\ \cup\ \text{watch}\} \Rightarrow \text{组合函数体}
  2. 封装组合函数相关逻辑打包为 useXxx 函数,函数内部统一管理状态与副作用。
    useXxx()={ref/reactive 状态, computed 派生, watch 副作用}\displaystyle \text{useXxx}() = \{\text{ref/reactive 状态},\ \text{computed 派生},\ \text{watch 副作用}\}
  3. 按需调用组件 setup 中调用 useXxx(),通过解构获取需要的状态与方法。
    setup: {a,b}=useXxx()\displaystyle \text{setup: } \{ a, b \} = \text{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 演示

python52 行
"""用 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
目的:用一个约40行的Python类模拟Vue3响应式核心——ref的依赖收集与派发更新、computed的缓存派生、watchEffect的自动追踪,帮你从原理层理解Composition API的运作方式。
  1. Ref._active = None类变量作为“当前正在运行的副作用指针”,是依赖收集的关键。
  2. @property def value用property让 count.value 读起来像普通属性,getter中完成依赖登记。
  3. self._effects.add(Ref._active)读取时把正在运行的effect登记为订阅者,这就是“收集依赖”。
  4. for fn in list(self._effects)写入时对订阅者集合做快照遍历,逐个通知更新。
  5. watch_effect(fn)先把fn设为全局活跃effect再执行它,让首次运行的读取操作自动完成收集。
  6. 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 函数在组件生命周期中的执行时机是?

单选题 · easy

  • A.beforeCreate 之前执行← 正确答案
  • B.created 之后才执行
  • C.mounted 之后才执行
  • D.首次渲染完成后执行

答案:A。setup在组件实例初始化最早期执行,早于beforeCreate和created。它替代了这两个旧选项的职责,所以实例尚未挂载、this无法使用。B、C、D都发生在setup之后。

Q2. 在 script 中读取 ref 创建的值,正确的方式是?

单选题 · easy

  • A.直接书写 count 即可
  • B.必须写 count.value← 正确答案
  • C.调用 count() 函数
  • D.用 this.count 访问

答案:B。ref创建的是包含value属性的对象,脚本中读写都要通过.value。模板中会自动解包可直接写count,但这是模板编译器的特殊处理;this.count因setup中无this而不可用。

Q3. 关于 ref 与 reactive,下列说法正确的有?

多选题 · mid

  • 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 对象进行解构后,取出的属性会?

单选题 · mid

  • A.保持原有深层响应性
  • B.丢失响应特性← 正确答案
  • C.自动转换成为 ref
  • D.变成普通函数

答案:B。解构取出的是当前值,脱离了Proxy代理,后续读取与赋值不再被拦截,响应性丢失。需要保持响应时用toRefs转换。自动转ref是不存在的,解构也不会变成函数。

Q5. computed 区别于普通方法的核心特性是?

单选题 · easy

  • A.每次访问都重新计算
  • B.依赖不变时返回缓存← 正确答案
  • C.能自动发起网络请求
  • D.只能返回布尔类型

答案:B。computed会追踪依赖并缓存结果,依赖不变时直接返回缓存,这是与普通方法最本质的区别。每次重算是method的特性;computed不应有网络请求副作用;返回类型任意。

Q6. watch 与 watchEffect 的核心区别是?

单选题 · mid

  • A.watch 立即执行且惰性
  • B.watch 需显式指定数据源← 正确答案
  • C.watchEffect 能拿到新旧值
  • D.watchEffect 是非响应式的

答案:B。watch必须显式声明source且默认惰性执行;watchEffect自动收集依赖并立即执行、拿不到旧值但写法简洁。A中“立即执行”描述错误,C、D与事实相反。

Q7. 以下场景更适合使用 watchEffect 的是?

多选题 · hard

  • A.需要在变化时访问旧值
  • B.多个依赖任一变都要重跑← 正确答案
  • C.想省去显式声明数据源← 正确答案
  • D.需要精确控制侦听对象

答案:BC。watchEffect自动收集所有读取的依赖,任一变化都重跑且拿不到旧值,所以B、C适合。需要旧值(A)或精确控制侦听对象(D)时,应使用watch。

Q8. 无法在 setup 中使用 this 的原因是?

单选题 · hard

  • A.组件实例尚未被创建
  • B.早期执行 this 未绑定← 正确答案
  • C.模板不再需要该实例
  • D.Vue3 中已废弃实例

答案:B。setup在实例初始化早期执行,此时实例尚未完成装配,this无法正确绑定到组件实例。组件实例本身依然存在,只是setup阶段无法通过this访问;A的说法不准确,C和D则与事实不符。

Q9. 下列属于 setup 合法参数的是?

多选题 · mid

  • 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 回调里异步读取响应式数据,会?

单选题 · extreme

  • A.同步与异步都会收集
  • B.只收集同步部分的依赖← 正确答案
  • C.不收集任何响应依赖
  • D.会收集全部已定义数据

答案:B。watchEffect只在回调同步执行期间收集依赖。异步回调执行时,Ref._active已经清空,此时读取的数据不会被登记,因此依赖变化时不会触发重跑。这是“依赖收集不完整”的经典陷阱,复杂异步逻辑应改用watch显式声明数据源。