ARTICLE DETAIL

资讯详情

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

Vue 3 ref 自动解包机制全解析:原理、边界与避坑指南

Vue 3 ref 自动解包机制全解析:原理、边界与避坑指南 写 Vue 3 写了几年ref 早就成了每天打交道最多的 API。但说实话真正把 ref 的自动解包机制吃透还是在踩了好几个坑之后。这个机制设计得很巧妙能让你在模板里少写一堆.value可它又没那么“智能”在特定场景下绕过你、坑你一把而且是那种报错都找不到原因的坑。这篇文章就把 ref 的特殊处理与解包机制完整拆一遍。我会从它的设计逻辑讲起再逐个场景过包括模板、reactive 对象、数组、嵌套 ref以及和 uni-app 这类框架结合时的实际情况。最后还会把我踩过的坑整理成速查表照着对就能少走弯路。1. 内容整体设计与思路拆解1.1 ref 的“双重视角”数据容器与响应式代理ref 本质上做了一件很朴素的事把普通值包装成一个带响应式能力的对象。这个对象内部维护一个value属性真正的数据存在value里访问和修改都绕不开它。但如果你在模板里写{{ count }}却能直接拿到数值不需要写{{ count.value }}——这就是自动解包机制的功劳。理解 ref 最好的方式是把它想象成一个“带监控的盒子”。你把东西放进盒子里盒子本身不能直接用但当你把它拿到特定场景比如模板时盒子会自动打开把里面的东西取出来给你用。这个“特定场景”就是解包机制生效的边界。这个设计和 Vue 2.x 的data返回对象有着本质区别。Vue 2 里数据是“透明”的对象里有什么就能拿到什么响应式是通过Object.defineProperty对属性做拦截实现的。Vue 3 把数据访问统一到.value上让响应式追踪变得更可控——至少在 JavaScript 层面是可控的。1.2 解包机制的设计意图让模板更干净让逻辑更清晰你可能会有疑问既然 ref 已经通过.value管理数据了为什么模板里还要自动解包这不是多此一举吗答案是模板的“读取频率”远高于逻辑代码。在页面上展示一个数据可能是在插值表达式、事件绑定、条件渲染、循环渲染里同时用到。如果每一处都要写.value模板会变得非常啰嗦可读性也会急剧下降。试想一下template div{{ count.value 0 ? count.value * 2 : 0 }}/div /template而有了自动解包template div{{ count 0 ? count * 2 : 0 }}/div /template两者都能运行但后面的代码明显清爽得多。这个设计的目的就是把模板场景下的“取值”简化为最自然的方式让模板关注展示把复杂的数据操作留在逻辑层。与之对应的是在script setup或setup()函数内部ref 的解包规则就完全不同了。你声明const count ref(0)然后count 1是行不通的因为此时count是 Ref 对象不是数值。JavaScript 的语法规则不会因为你用了 Vue 就改变所以这里必须老老实实写count.value 1。1.3 “万能对象”的真相ref 到底能包什么网上有关“ref 万能对象”的讨论不少这其实说的是 ref 对数据类型的包容度。ref 的底层是通过ref()函数创建一个 RefImpl 实例这个实例的value可以是任何值——基本类型、对象、数组甚至是另一个 ref。const num ref(0) const obj ref({ name: vue, version: 3 }) const arr ref([1, 2, 3]) const nested ref(ref(0))关键在于当ref()接收一个对象时它会用reactive()对这个对象做深度响应式代理。也就是说const obj ref({ name: vue, nested: { a: 1 } })你改变obj.value.name或obj.value.nested.a都会触发响应式更新。所以“万能对象”并不是说 ref 变成了 Reactive 那种直接代理对象而是说ref 不仅能管基本类型也能管复杂结构而且内部的响应式能力是完整的。拿这个特性去做全局状态管理、表单数据维护、复杂配置对象管理都比 reactive 更灵活。因为 ref 的数据访问方式统一都得.value在某些场景下反而更容易理解和维护。2. 核心细节解析与实操要点2.1 模板中的自动解包顶层属性与嵌套属性的差异模板中的自动解包有个很重要的前提只能是顶层属性。这句话怎么理解假设你有这样的 setup 返回值setup() { const count ref(0) const obj { count: ref(0) } return { count, obj } }在模板里template div{{ count }}/div div{{ obj.count }}/div /template第一行count是 setup 返回的顶层属性会被自动解包显示数字。第二行obj.count就不是顶层属性了它是对象obj的属性而obj是一个普通对象不是 reactive此时obj.count是 Ref 对象不会自动解包。模板会显示[object Object]或者直接渲染空看具体版本和渲染方式。这就有点反直觉了我没写.value为什么不出来原因在于模板编译器的解包逻辑。Vue 3 的模板编译器会将{{ count }}编译为类似_ctx.count的访问然后对 ref 做unref处理。但对于obj.count编译器是按普通对象属性访问来处理的它不知道obj.count本身是一个 ref。在script setup语法中有一个类似但更严格的规则只有顶层变量才会在模板中自动解包。比如script setup import { ref } from vue const count ref(0) /script template div{{ count }}/div !-- 自动解包显示 0 -- /template但如果嵌套在一个非 ref 对象里script setup import { ref } from vue const obj { count: ref(0) } /script template div{{ obj.count }}/div !-- 不会解包 -- /template这里官网文档建议大家用toRefs或者直接ref声明来进行状态管理尽量不要把 ref 塞进普通对象里不然模板里的显示会和你预期不一致。2.2 reactive 对象中的解包规则浅层解包与覆盖行为reactive 对象内部对 ref 有一套更“聪明”但也更严格的解包规则。当一个 ref 作为属性被放进 reactive 对象时访问该属性时会自动解包const count ref(0) const state reactive({ count }) console.log(state.count) // 0自动解包不需要 .value state.count 1 console.log(state.count) // 1 console.log(count.value) // 1同步更新这个机制依赖于 reactive 对象的属性访问拦截。当你访问state.count时Vue 内部会检测到这个属性值是一个 ref就自动帮你做了unref。但这个解包有两个关键限制第一只发生在 reactive 对象内部不发生在 set 的时候。如果你直接给state.count赋一个新的 refstate.count ref(2) console.log(state.count) // 2 console.log(state.count.value) // undefined赋值时不会自动把ref(2)包一层再解包而是直接替换为新值。这里要注意state.count和原始的count已经脱离了联系因为 reactive 对象内部存储的是ref(2)这个引用而访问时解包得到2。第二解包是浅层的。reactive 对象的第一层属性如果是 ref访问时会解包。但是如果 ref 嵌套在更深的地方const state reactive({ nested: { deepRef: ref(1) } }) console.log(state.nested.deepRef) // Ref 对象不解包ref(1)存在于nested对象内部nested是被 reactive 代理过的但属性deepRef并不会被自动解包——解包只发生在reactive(obj)的自身属性层面reactive 对嵌套对象的代理不会继续“透传”解包逻辑。我在实际开发中遇到过最典型的情况把多个 ref 组织成一个 reactive 的 store 对象然后在某个异步回调里给其中一个属性赋值另一个模块读取时发现是 ref 对象而不是值报错很难定位。后来养成了习惯统一从 ref 的.value层面取数据或者明确只通过 reactive 的第一层属性访问数据混用模式下很难保证行为的稳定。2.3 数组与集合容器为什么不会被解包这是解包机制里最经典的一个“坑”ref 包出来的数组如果放进 reactive 对象数组里的 ref 不会被自动解包。const arr reactive([ref(0), ref(1), ref(2)]) console.log(arr[0]) // Ref 对象 { value: 0 }不是 0 console.log(arr[0].value) // 0你可能会想数组也是对象reactive 代理了数组之后访问arr[0]应该自动解包才对。但 Vue 的源码里并没有为数组元素做解包处理。原因可能有两个一是数组索引访问是一种“非常热”的操作如果每个索引访问都要做一次 isRef 判断性能会明显下降二是数组元素的语义更复杂——你可能真的要存一个 ref 对象在数组里如果自动解包了反而会破坏这种用途。这个行为在日常开发中的影响是巨大的。很多人会用数组来管理表单字段、列表行数据想在数组里放 ref 来维护每一行的状态结果取出来全是 ref 对象还得手动.value代码变得很丑。我的实用建议是数组里不要放 ref直接放普通值或者 reactive 对象。如果要给每个数组项维护独立状态那就把整项做成 reactive 对象const list reactive([ { name: a, count: 1 }, { name: b, count: 2 } ])或者用 ref 包裹整个数组const list ref([{ name: a, count: 1 }]) console.log(list.value[0].name) // 需要 .value这样至少数据结构是一致的不会出现数组里有 ref 和非 ref 混在一起的混乱局面。2.4 嵌套 ref 与解包边界什么时候不自动解包再来看看嵌套 ref 的情况。嵌套 ref 就是“ref 里面套 ref”const outer ref(0) const inner ref(outer) console.log(inner.value) // 0而不是 Ref 对象这是 Vue 3 的一个特殊设计ref 初始化时如果 value 是一个 ref会自动解包一层。也就是说inner.value直接拿到的是数字0。这个设计在源码里体现为class RefImpl { constructor(value) { this._value toRawReactive(value) } get value() { return toRaw(this._value) } }流程很简单ref 接收值后如果值是 ref就通过toRaw取出内部值如果isRef(value)为真toRawReactive会返回value.value。所以嵌套 ref 最多解一层不会递归无限解包。对应地在模板里template div{{ inner }}/div !-- 显示 0 -- /template但如果嵌套层级更深呢const a ref(0) const b ref(a) const c ref(b) console.log(c.value) // 0仍然解包这里的处理方式是把c.value用unref解包后再作为 Ref 的 value 存储。不过说实话我在实际业务里没见过有人真的写这么深的嵌套这更多是库内部实现逻辑。了解它能帮你理解“为什么 ref 的 value 拿到的值是数字而不是 Ref 对象”这在调试第三方库代码时很有用。3. 实操过程与核心环节实现3.1 场景一setup 返回 ref 与模板绑定先看最经典的日常用法。用标准的setup()函数组织逻辑并配合模板绑定template div classcounter p当前计数{{ count }}/p p翻倍计数{{ doubleCount }}/p button clickincrement增加/button /div /template script import { ref, computed } from vue export default { setup() { const count ref(0) const doubleCount computed(() count.value * 2) function increment() { count.value } return { count, doubleCount, increment } } } /script这里模板里的{{ count }}是 setup 返回的顶层属性自动解包显示数字。{{ doubleCount }}同理computed 返回的也是 ref 对象在模板里解包为数值。关键点在于函数的返回值如果是 setup 的顶层属性解包机制才会生效。但因为increment是一个函数它不会被解包可以直接在模板里绑定事件。这种写法和script setup的差异主要在于代码组织粒度。script setup相当于自动把这个模块里所有顶层声明暴露给模板少一层手动 return但语义是相似的。3.2 场景二复杂对象管理中的 ref 解包我在实际项目中做过一个配置管理模块用 ref 封装整个配置对象然后通过 computed 派生出需要的数据。代码大致是这样script setup import { ref, computed, watch } from vue const config ref({ theme: dark, fontSize: 14, ui: { showSidebar: true, density: medium }, behaviors: [] }) const fontSizePx computed(() ${config.value.fontSize}px) function updateTheme(newTheme) { config.value.theme newTheme // 这里不需要深层逐层加 .value因为 config.value 是 reactive 代理 } /script template div classsettings p当前主题{{ config.theme }}/p p字号{{ fontSizePx }}/p button clickupdateTheme(light)切换主题/button /div /template这里有个很多人会搞混的细节ref 包裹对象时只有第一层需要用.value访问内部属性访问不需要。因为ref({ ... })的底层实现会把对象转为 reactive 代理config.value本身就是一个 reactive 对象访问config.value.theme会触发响应式追踪同时config.value.ui.showSidebar这种深层访问也有效。注意模板里的{{ config.theme }}——它不是自动解包的结果而是因为config在模板里先解包为 reactive 对象然后.theme才能取到值。如果你在模板里只写{{ config }}会看到一个 Proxy 对象不会自动变成字符串。这一点很容易被忽略自动解包只发生在 ref 对象本身这一层解包后的对象如果有内部属性属性访问的逻辑按普通 JS 对象规则走。3.3 场景三switch 场景中的解包逻辑与强制绑定解包机制不只是取值它还会影响赋值。最典型的就是v-model和事件绑定中的表现。先看一个我认为能解释“ref 解包是响应式核心”的案例。在switch场景里比如设置页的开关切换script setup import { ref } from vue const notifications { email: ref(true), sms: ref(false), push: ref(true) } /script template el-switch v-modelnotifications.email / /template这段代码有问题吗从语法上看没问题但实际运行时你会发现开关状态改变后模板里的其他绑定可能不生效。原因就是前面提到的notifications是普通对象notifications.email作为其属性在响应式系统内部不会自动解包。最安全的做法是直接使用 reactivescript setup import { reactive } from vue const notifications reactive({ email: true, sms: false, push: true }) /script template el-switch v-modelnotifications.email / /template或者用 toRefs 把 reactive 对象拆成多个 ref 再组合script setup import { reactive, toRefs } from vue const notifications reactive({ email: true, sms: false, push: true }) const { email, sms, push } toRefs(notifications) /script template el-switch v-modelemail / /template在 uni-app Vue 3 项目中这个场景更常见。页面数据放reactive配合toRefs在模板里使用。因为 uni-app 的页面生命周期和普通 Vue 组件有差异数据必须以可预测的方式暴露给视图层这时候模板解包机制的可靠性就很重要——它不能依赖“某种情况下解包某种情况下不解包”的不确定性。3.4 热词里的“强势区域”和 ref 计算在热词里有一条看起来像量化交易条件的字符串“强势区域1:(count(every(bmacd00,1),7)3 or count(every(bmacd0ref(bmacd0,1) ...”这里出现了ref作为计算函数名的用法。我要明确地指出一个概念边界量化/技术分析框架中的 ref 函数和 Vue 3 里的 ref API 不是一回事。在股票或期货数据分析软件里ref(X, N)表示取 X 在 N 个周期前的值。比如ref(close, 1)是昨天的收盘价。它和前端框架的 ref 只是名称相同语义完全不同。但这两个领域有个共同点都是对“取值”的处理。技术分析里的ref(close, 1)是在时间轴上取前值Vue 里的 ref 是在响应式数据上取值。如果你们团队同时做前端和量化分析文档里看到ref时一定要先确认上下文是哪个领域别被相同的函数名带偏。就拿every(bmacd00,1)来说它的意思是“最近 1 根K线满足 bmacd00”而count(...,7)3表示“近 7 根K线里满足该条件的天数为 3”。整体条件组合了多个时间窗口的判断这种写法的时间序列逻辑和响应式拆包是完全不同的思维模式。在量化领域ref 取前值后通常不会改写原值而 Vue 的 ref 是可以双向绑定的。写代码时别混着用。3.5 数组循环中的 ref 解包处理这是我最想分享的实战细节之一。平时用v-for循环渲染列表数据时如果数组由 ref 包裹循环内你要访问的是item而不是item.valuescript setup import { ref } from vue const todos ref([ { id: 1, text: 学习 ref, done: false }, { id: 2, text: 掌握解包, done: false } ]) /script template ul li v-fortodo in todos :keytodo.id input typecheckbox v-modeltodo.done / span :class{ done: todo.done }{{ todo.text }}/span /li /ul /template这里的v-fortodo in todos就涉及对象解包的边界。todos是 ref 包裹的数组在模板里是第一层属性自动解包所以todo是数组元素——普通对象。接下来todo.done的访问就是普通对象属性访问不需要再写.value。这个场景容易出问题的版本是把 ref 放在数组里再循环。比如script setup import { ref } from vue const list ref([ref({ name: a }), ref({ name: b })]) /script template ul li v-foritem in list :keyitem {{ item.name }} /li /ul /template这里模板里{{ item.name }}一定能生效吗不一定。关键在于list解包出来的数组里的元素是 ref 对象但v-for 循环内不会继续解包所以item仍是 Ref 对象item.name是undefined。模板里的{{ item.name }}实际上访问的对象属性根本不存在。我在写这类逻辑时如果发现列表超预期地没有渲染出内容第一反应就是排查数据层级。合理的方式是写v-foritem in list.value—— 但这在模板里做不到因为模板里list已经解包过了。所以最好的方式还是保证数组元素本身不是 ref普通对象加 ref 包裹外层才是正道。4. 常见问题与排查技巧实录4.1 为什么模板里显示 [object Object]这是最常见的问题没有之一。模板中原本应该显示对象里某个字段结果渲染成了[object Object]。排查思路很直接确认模板里访问的是否是顶层 ref 属性。如果不是顶层属性检查它是否在普通对象里。如果是{{ obj.refProp }}的形式说明obj.refProp是 Ref 对象模板不会自动解包。修复方式有两种模板里手动加.value{{ obj.refProp.value }}数据结构上避免这种写法直接用toRefs或 setup 顶层返回script setup import { ref, toRefs } from vue const obj reactive({ count: ref(0) }) const { count } toRefs({ count: obj.count }) // 提取为独立 ref 顶层返回 /script template div{{ count }}/div /template我在实际开发中倾向用 toRefs 或直接多写几个 ref 变量而不用“普通对象 ref 属性”的组合。因为模板问题排查太浪费时间了不如一开始就规避。4.2 为什么 ref 赋值后视图不更新这个问题的坑不在 ref 本身而在它对对象的处理方式。先看代码const obj ref({}) obj.value.name vue这行代码能触发视图更新吗能前提是obj.value一开始就是 reactive 代理。因为ref({})内部已经把空对象转成了 reactive之后给obj.value.name赋值会走 reactive 的 set 拦截触发更新。但另一种写法不行const obj ref({}) obj.value { name: vue }这种会触发更新吗也会。因为 ref 的.value赋值本身就是响应式行为。两种情况都正常真正不正常的场景是把 ref 塞进普通对象却不触发const myRef ref(0) const plainObj { myRef } plainObj.myRef 1 // 不会触发任何响应式更新这个坑在跨组件传参时往往爆出来。你把 ref 放在普通对象里传给子组件子组件修改它父组件没有反应因为普通对象没有响应式能力。要解决就得用 reactive、toRefs、provide/inject、状态管理库等正式方案千万别把响应式数据塞进“普通容器”里就完事了。4.3 为什么 v-model 绑定的值变成了 ref 对象本身在使用自定义组件时v-model 的默认 prop 是modelValue默认事件是update:modelValue。如果你把 ref 作为 modelValue 传给子组件子组件内部对它做赋值操作那么!-- 父组件 -- ChildComponent v-modelcount / !-- 子组件 -- script setup const props defineProps([modelValue]) const emit defineEmits([update:modelValue]) function onChange(e) { emit(update:modelValue, props.modelValue 1) } /script正常情况下父组件的count是 refv-model会在模板里自动解包所以在子组件里props.modelValue是数值而不是 ref 对象。这里解包机制已经生效了没有坑。但当你完全绕过 v-model手动传整个 ref 对象时ChildComponent :model-valuecount /在子组件里props.modelValue就是 ref 对象不是数值。如果你不知道这一点拿它去加减乘除、显示就会得到奇怪的结果。所以自查清单就一条模板绑定里 v-model 自动解包但 props 穿对象时不会自动解包 ref 本身。4.4 ref 与 reactive 混用时的监听失衡问题混用 ref 和 reactive 时watch 行为也可能出问题。看这段const countRef ref(0) const state reactive({ countRef }) watch(countRef, (val) { console.log(countRef changed, val) }) watch( () state.countRef, (val) { console.log(state.countRef changed, val) } ) state.countRef这个例子中两个 watch 都能触发吗取决于你修改的是哪个引用。如果通过countRef.value两个 watch 都会触发因为state.countRef解包后访问的是同一个内部值。但如果直接赋值state.countRef 10那countRef本身没变第一个 watch 不会触发第二个 watch 会触发。这类监听失衡问题在状态联动比较复杂时非常隐蔽。我的建议是一个数据源一条修改路径。如果一定要同时存在 ref 和 reactive 的访问那就统一用ref.value的方式修改避免走 reactive 属性赋值绕开了原始 ref 的更新通知。4.5 常见问题速查表场景现象原因解决方案普通对象包含 ref模板展示显示 [object Object]非顶层属性不自动解包用 reactive 替代普通对象或用 toRefsreactive 对象数组内含 refarr[0]是 ref 对象数组元素不解包数组内不放 ref放普通值/reactive对象普通对象包含 ref 并赋值视图不更新普通对象无响应式用 reactive、ref 顶层管理状态props 传整个 ref 对象子组件拿到 Ref 对象props 传参不自动解包传值或传 reactive 对象ref 包裹数组v-for 内访问item 取不到属性循环内不解包数组元素数组内元素用普通对象v-model 绑定普通对象里的 ref状态可改但模板不响应非响应式容器改用 reactive 或 ref 顶层声明4.6 我的排查方法论说点实在的。遇到 ref 相关的显示或更新问题我有一套固定排查顺序基本能解决九成问题第一步确认数据的存储方式。是 ref、reactive还是拼在普通对象里这个决定了解包机制是否参与。第二步确认访问路径。是直接访问顶层 setup 返回的 ref还是通过对象属性/数组索引间接访问这决定解包是否生效。第三步确认修改路径。改的是.value还是 reactive 的某个属性还是普通对象的属性这决定响应式系统能否捕获到变化。第四步用控制台验证。打开浏览器控制台打印一下访问的值看它是不是 Ref 对象。如果是就按前面的规则去找为什么解包没生效。这个方法最朴素但最有效没有例外。5. uni-app 场景下的 ref 解包实战5.1 uni-app Vue 3 中 ref 的表现差异uni-app 的 Vue 3 版本底层是基于 Vue 3 的响应式系统的所以 ref 的解包机制和纯 Vue 3 大体一致。但在小程序端和 H5 端存在一些细微差异主要在于模板编译的差异——uni-app 的模板编译器会针对不同平台生成不同的渲染代码个别场景下解包行为的边界可能和纯 Web 端略有不同。比较典型的场景是在小程序端ref包reactive对象再传给子组件子组件里通过props拿到的行为在微信小程序里可能会多一些序列化/反序列化的步骤导致 ref 解包后的对象不再是原来的 Proxy。这种问题在真机调试时非常容易出现。我通常的做法是状态管理统一用 ref 或 reactive 之一不混用组件通信保持最小化能不用 props 传复杂对象就不用。5.2 uni-app 页面生命周期与 ref 解包的配合uni-app 的页面有个特点它的生命周期onLoad、onShow等是框架层面的方法不是 Vue 组件钩子。页面里用ref()声明的数据在onLoad中赋值在模板中使用走的仍然是 Vue 的响应式流程解包机制完全生效。但要注意一个边界onLoad中如果有异步操作比如网络请求回来后再给 ref 赋值那么模板中绑定这个 ref 的地方会正常响应。因为响应式系统的依托是 Proxy 和依赖收集和时间是否异步没关系。我在实际项目里遇到过一个问题在onLoad里给ref([])赋值一个普通数组页面模板里v-for渲染正常。但如果我直接在onLoad里写data.value.push(...)在某些低版本微信基础库上动态插入元素会导致渲染异常。排查后发现这是setter 触发依赖更新的滞后期造成的不是 ref 本身的问题。所以 uni-app 下的一个经验是组件数据变更宁可多走一步“整值替换”也不要依赖数组/对象的局部突变。这虽然牺牲了一点响应式的“精细度”但换来了跨端渲染的稳定性。5.3 跨端数据管理的推荐组合我推荐一个在 uni-app 项目中稳定跑了很久的状态管理组合全局状态用 reactive 定义 store 对象配合 computed 派生数据组件内页面数据用 ref 管理模板直接访问跨组件传递用 provide/inject但保证注入的不是普通对象而是 ref 或 reactive// store.js import { reactive, computed } from vue export const store reactive({ userInfo: null, settings: {}, theme: computed(() store.settings.theme || light) }) export function setUserInfo(info) { store.userInfo info }组件内script setup import { ref, computed } from vue import { store } from /store const pageTitle ref(首页) const displayName computed(() store.userInfo?.name || 游客) /script template view{{ pageTitle }}/view view{{ displayName }}/view /template这样组合的优点是状态源清晰、解包边界稳定、模板层的自动解包机制能顺畅工作。同一个数据如果既出现在全局 store 又出现在局部 ref 里最糟糕的情况就是“一个数据、两个源头”改动时必须手动同步。6. 解包机制的边界与上手经验总结如果把所有规律浓缩成一句话那就是ref 的自动解包是“按容器”的不是“按类型”的。模板只解包 setup 顶层属性reactive 只解包自身属性数组不解包普通对象不解包。记住这个底层规律大部分问题都不再是问题。在实际开发中我总结了几条经验可能对你有帮助。第一条能不用普通对象装 ref就别用。普通对象没有响应式能力里面放 ref 只会制造混乱。会用reactive就用喜欢ref就坚持 ref别混搭。第二条** ref 包对象后内部属性访问很自然但对象替换要谨慎**。ref({ a: 1 })之后你可以直接改ref.value.a响应式没问题。但如果你整体替换ref.value { b: 2 }那原来对ref.value.a的依赖关系会断开模板里绑定ref.value.a的地方会变成 undefined这种错误在运行时不会报红只会静默出 bug很难查。第三条数组场景下用 ref 包整个数组而不是每个元素各一个 ref。这不仅是解包规则的要求也是性能考虑——每个 ref 都需要建立代理和依赖关系一百个元素一百个 ref 的开销远大于一个数组包一层 reactive。第四条组合式函数返回响应式数据时注意返回值的形态。如果你写一个useCounter返回{ count: ref(0), increment }那调用方在模板里必须通过count顶层属性访问。如果你返回的是ref({ count: 0 })模板里就要访问count.count。这两种形态在文档、类型定义和调用方式上都不同最好在函数设计阶段就统一风格。我更喜欢返回一个 ref 对象本身然后让调用方决定怎么解包function useCounter() { const state ref({ count: 0 }) function increment() { state.value.count } return { state, increment } }或者function useCounter() { const count ref(0) function increment() { count.value } return { count, increment } }两种风格都能用关键是一致性。团队里不统一才是最大的坑。最后分享一个小技巧如果你在写代码时拿不准某个值在模板里会不会被自动解包可以在模板里放一个测试输出template pre{{ JSON.stringify(someRef) }}/pre /template如果输出了对象而不是值那说明它在这个场景下没有被解包。这个方法本质上不过是“打印出来看看”但它能在开发初期就帮你确定解包边界省掉后面大量的排错时间。ref 的解包机制并不是一个需要死记硬背的规则表——它不过是一套“哪些容器对 ref 更友好”的设计策略。理解了容器的边界就理解了整个机制。
返回列表