- 前端
【免费下载链接】vueuse
Collection of essential Vue Composition Utilities for Vue 3
extendRef是 VueUse(packages/shared/extendRef)中一个精巧的 Reactivity 工具函数:它允许你在不修改原有 Ref 行为的前提下,为 Ref 对象动态附加自定义属性,并且这些附加属性在传入Ref值时会自动解包、保持响应式联动。阅读本文后,你将掌握extendRef的完整用法、两个核心配置项(enumerable与unwrap)的底层语义,以及它是如何成为refWithControl等高级响应式工具的地基。
功能定位:给 Ref 添加"额外属性"
在 Vue 3 中,Ref本质上是一个带有value属性、并且value具备响应式能力的对象。但很多场景下,我们希望一个 ref 除了携带数据之外,还能顺带携带一些"元数据"或配套方法——例如给ref挂上一个get/set函数、一份说明文字或关联的辅助对象。
extendRef(ref, extend, options?)正是为此而生:它以第一个参数ref为基底,把第二个参数extend对象上的所有属性"合并"进这个 ref 本身,并返回同一个 ref 对象(ref的引用没有被替换,只是原地扩展)。
快速上手:最简单的扩展用法
原文档给出了最直观的使用示例——先构造一个shallowRef,再用extendRef挂上一个普通字符串属性:
import { extendRef } from '@vueuse/core' import { shallowRef } from 'vue' const myRef = shallowRef('content') const extended = extendRef(myRef, { foo: 'extra data' }) extended.value === 'content' extended.foo === 'extra data'执行完成后,extended仍然是原来的那个 ref:extended.value照常读取/写入原始内容;同时extended.foo可以像普通对象属性一样被访问。
需要注意的关键约束(原文档明确强调):
附加的属性在 Vue 的模板中是不可访问的。
原因在于 Vue 模板对 ref 的解包只针对value这一约定属性,extendRef附加的其他键并不会参与模板编译期的解包逻辑。因此这类扩展属性更适合在组合式函数内部、watch/computed/普通 JS 逻辑中消费,而不是直接暴露给模板。
附加属性传入 Ref:自动解包与双向响应式
extendRef更强大的能力在于:当extend对象中的某个属性值本身是一个Ref时,它会把这个属性"解包"为普通值,并保持与源 ref 的双向响应式同步。原文档示例:
import { extendRef } from '@vueuse/core' import { shallowRef } from 'vue' const myRef = shallowRef('content') const extraRef = shallowRef('extra') const extended = extendRef(myRef, { extra: extraRef }) extended.value === 'content' extended.extra === 'extra' extended.extra = 'new data' // 赋值会触发更新 extraRef.value === 'new data'这里extended.extra读到的是extraRef.value("extra" 而非 ref 对象本身);而当你给extended.extra赋值时,值会被写回extraRef.value,从而触发依赖该 ref 的响应式更新。也就是说,扩展出的属性与源 ref 是双向联动的,这为实现"带控制的 ref"(见下文refWithControl)提供了基础机制。
配置项详解:enumerable与unwrap
extendRef接受一个可选的第三个参数,其类型定义位于源码的 ExtendRefOptions 接口:
export interface ExtendRefOptions<Unwrap extends boolean = boolean> { /** * 扩展属性是否可枚举 * * @default false */ enumerable?: boolean /** * 是否对 Ref 类型的属性值进行解包 * * @default true */ unwrap?: Unwrap }| 选项 | 默认值 | 作用 |
|---|---|---|
enumerable | false | 控制扩展属性是否出现在Object.keys、for...in、展开运算符等枚举操作中。默认不可枚举,避免污染对 ref 的常规遍历 |
unwrap | true | 控制传入extend的Ref值是否被解包为value。默认解包并建立 getter/setter 双向同步;设为false则原样挂载 ref 对象本身 |
源码级原理:Object.defineProperty的两次分派
从源码结构看,extendRef的实现非常克制,核心逻辑只有一段循环(见 extendRef 实现):
export function extendRef<R extends Ref<any>, Extend extends object>( ref: R, extend: Extend, { enumerable = false, unwrap = true }: ExtendRefOptions = {}, ): ExtendRefReturn<UnwrapRef<R>> { for (const [key, value] of Object.entries(extend)) { if (key === 'value') continue if (isRef(value) && unwrap) { Object.defineProperty(ref, key, { get() { return value.value }, set(v) { value.value = v }, enumerable, }) } else { Object.defineProperty(ref, key, { value, enumerable }) } } return ref }可以提炼出三个关键设计:
- 跳过
value键:if (key === 'value') continue是一道安全闸门——value是 ref 的核心访问器,extendRef坚决不碰它,避免覆盖 Vue 的响应式语义。 - Ref 值走 getter/setter 分支:当属性值是 ref 且
unwrap为true时,通过Object.defineProperty定义一对访问器:get()读取value.value,set(v)写入value.value。由于写入的是源 ref 的value,Vue 的依赖收集与派发更新机制自然被触发——这就是"赋值会触发更新"的底层来源。 - 普通值走数据属性分支:非 ref 属性(或
unwrap: false时的 ref)则直接以数据属性挂载,enumerable决定其是否可被枚举。
最终函数返回ref本身(原地扩展),因此返回值在isRef检查下依然成立,完全兼容原有 ref 的一切用法。
类型层面的两套重载
为了让 TS 用户获得精确的类型推断,extendRef声明了两套函数重载(见 源码重载声明):
// Overload 1:unwrap 显式设为 false export function extendRef< R extends Ref<any>, Extend extends object, Options extends ExtendRefOptions<false>, >(ref: R, extend: Extend, options?: Options): ShallowUnwrapRef<Extend> & R // Overload 2:unwrap 未设置或设为 true export function extendRef< R extends Ref<any>, Extend extends object, Options extends ExtendRefOptions, >(ref: R, extend: Extend, options?: Options): Extend & Runwrap: false时,返回类型为ShallowUnwrapRef<Extend> & R:附加属性保持"浅解包"的 ref 形态,便于你显式访问 ref 对象。unwrap为true(默认)时,返回类型为Extend & R:附加属性直接表现为普通值类型,与运行时解包行为保持一致。
两个重载的交叉类型都包含R,从类型层面保证了返回值仍是原来的 ref。
实战印证:refWithControl如何消费extendRef
extendRef并非孤立存在,它是 VueUse 内部多个高级工具的实现地基。最典型的例子是 refWithControl(源码) ——它用customRef构造一个可精细控制跟踪/触发的 ref,然后借助extendRef把get、set、untrackedGet、silentSet、peek、lay六个控制方法挂到 ref 上:
return extendRef( ref, { get, set, untrackedGet, silentSet, peek, lay, }, { enumerable: true }, )注意这里显式传入了{ enumerable: true },因为refWithControl期望这些方法对使用者可见、可枚举。其配套文档 refWithControl 使用说明 展示了典型场景:
import { refWithControl } from '@vueuse/core' const num = refWithControl(0) const doubled = computed(() => num.value * 2) num.value = 42 console.log(num.value) // 42 console.log(doubled.value) // 84 // 不触发响应式更新的赋值 num.set(30, false) console.log(num.value) // 30 console.log(doubled.value) // 84(未更新)refWithControl的测试用例(index.test.ts)也从侧面验证了extendRef挂载属性的行为符合预期:expect(isRef(ref)).toBe(true)确认扩展后的对象仍是 ref;ref.lay(42)、ref.silentSet(42)之后ref.value变为 42 但watch回调不执行,证明扩展出的set系列方法确实绕过了触发机制;onBeforeChange返回false可以驳回变更,等等。这说明extendRef定义在 ref 上的扩展属性与 Vue 的响应式体系完全兼容、可被正常测试与依赖。
使用注意与适用边界
综合原文档与源码,使用extendRef时有几点值得留意:
- 模板不可访问:扩展属性只存在于 ref 对象上,模板解包机制不会识别它们,请在组合式函数/JS 逻辑中消费。
value键被保留:extend对象中名为value的键会被静默跳过,无法用来覆盖 ref 的核心值。- 原地扩展、返回原对象:函数不创建新 ref,返回的是传入的同一个 ref,因此
extendRef前捕获的引用也能看到扩展后的属性。 enumerable默认关闭:如果不显式开启,扩展属性不会出现在Object.keys/ 展开运算中;refWithControl这类需要暴露方法的场景则应显式传{ enumerable: true }。- 导入路径:
extendRef通过 packages/shared/index.ts 统一导出,可从@vueuse/core直接引入。
如果你需要的是"既能控制何时跟踪/触发响应式、又暴露精细方法"的 ref,直接使用基于extendRef构建的refWithControl会更省力;而当你只想为某个自定义 ref 轻量挂载元数据或辅助函数时,extendRef本身就是一个足够干净、低侵入的解决方案。
- 前端
【免费下载链接】vueuse
Collection of essential Vue Composition Utilities for Vue 3
相关推荐
VueUse useCloned 深入解析:为 Ref 打造响应式克隆副本的完整实战指南
VueUse useCloned 深入解析:为 Ref 打造响应式克隆副本的完整实战指南 useCloned 是 VueUse 核心包中一个轻量但极为实用的工具
前端5 步部署 AzerothCore:魔兽世界私服从零跑起来
5 步部署 AzerothCore:魔兽世界私服从零跑起来 一个周末,把一台 3.3.5 的魔兽世界私服拉起来:不用买服务器、不用手搓环境,AzerothCor
游戏开发后端VueUse Electron 扩展包:用 Composition API 响应式封装 Electron 渲染进程能力
VueUse Electron 扩展包:用 Composition API 响应式封装 Electron 渲染进程能力 @vueuse/electron 是 V
前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考