- 前端
【免费下载链接】vueuse
Collection of essential Vue Composition Utilities for Vue 3
unrefElement是 VueUse 核心库中一个轻量但极其关键的底层工具函数:它能够从模板 ref、普通 ref、getter 乃至 Vue 组件实例中,稳定地取出其背后的真实 DOM 元素。在 Vue 3 生态中,大量 DOM 相关 composable(如useElementSize、onClickOutside、useCurrentElement)都依赖它统一元素解析逻辑。读完本文,你将掌握unrefElement的输入类型体系、内部实现原理、边界行为与典型实战用法。
为什么需要 unrefElement
在 Vue 3 中,模板 ref 存在两类截然不同的绑定结果:
- 绑定到原生元素(如
<div ref="div" />)时,ref 持有的是HTMLElement / SVGElement 实例; - 绑定到组件(如
<HelloWorld ref="hello" />)时,ref 持有的是Vue 组件实例(ComponentPublicInstance),真实的 DOM 根节点需要访问该实例的$el属性才能取得。
如果业务代码直接编写ref.value.$el,就必须在每次使用时自行判断 ref 究竟指向元素还是组件实例,容易出错且难以维护。unrefElement正是为解决这一痛点而生:它接受"元素 ref 或组件实例",并统一返回底层 DOM 元素。官方文档对其功能定位的描述是:
Retrieves the underlying DOM element from a Vue ref or component instance
即:从 Vue ref 或组件实例中检索其底层的 DOM 元素。
快速上手:一个示例看懂用法
原文档给出了最典型的用法场景——在组件挂载后同时解析原生元素 ref 与组件 ref:
<script setup lang="ts"> import { unrefElement } from '@vueuse/core' import { onMounted, useTemplateRef } from 'vue' const div = useTemplateRef('div') // will be bound to the <div> element const hello = useTemplateRef('hello') // will be bound to the HelloWorld Component onMounted(() => { console.log(unrefElement(div)) // the <div> element console.log(unrefElement(hello)) // the root element of the HelloWorld Component }) </script> <template> <div ref="div" /> <HelloWorld ref="hello" /> </template>要点拆解:
divref 绑定原生<div>,unrefElement(div)直接返回该HTMLDivElement;helloref 绑定HelloWorld组件,unrefElement(hello)返回该组件的根 DOM 元素;- 示例中使用 Vue 3.5+ 的
useTemplateRef声明模板 ref(在 Vue 3.5 之前的写法是const div = ref()),两者均可作为unrefElement的合法输入; - 解析动作放在
onMounted中执行,这是关键前提——只有组件挂载完成后,$el与元素 ref 才会被真实赋值。
源码实现:核心逻辑仅一行
unrefElement的实现极其精简,完整源码位于 packages/core/unrefElement/index.ts:
export function unrefElement<T extends MaybeElement>(elRef: MaybeComputedElementRef<T>): UnRefElementReturn<T> { const plain = toValue(elRef) return (plain as VueInstance)?.$el ?? plain }整个解析过程只有两步:
toValue(elRef)解包:toValue是 Vue 3.3+ 提供的统一取值函数,能同时处理普通值、ref、getter 函数三种输入形态,等价于旧写法isRef(elRef) ? elRef.value : typeof elRef === 'function' ? elRef() : elRef。这意味着你可以直接传入useTemplateRef('div')得到的 ref,也可以传入() => document.querySelector('#app')这样的 getter;$el兜底解析:如果解包后的值是 Vue 组件实例(具有$el属性),则返回$el;否则原样返回(此时它通常已经是原生元素或null/undefined)。利用空值合并运算符??,即使实例存在但$el为空,也会优雅地回退到原值。
这种"先解包、后探测$el"的两段式设计,让unrefElement对三类输入(元素、组件实例、空值)都能给出合理结果,且不依赖任何 DOM 环境判断,天然兼容服务端渲染(SSR)。
输入类型体系
在 packages/core/unrefElement/index.ts 中,unrefElement及相关函数共同依赖一套精心设计的类型别名:
export type VueInstance = ComponentPublicInstance export type MaybeElementRef<T extends MaybeElement = MaybeElement> = MaybeRef<T> export type MaybeComputedElementRef<T extends MaybeElement = MaybeElement> = MaybeRefOrGetter<T> export type MaybeElement = HTMLElement | SVGElement | VueInstance | undefined | null export type MaybeComputedElementRefOrArray<T extends MaybeElement = MaybeElement> = | MaybeComputedElementRef<T> | MaybeComputedElementRef<T>[] | MaybeRefOrGetter<T[] | null> export type UnRefElementReturn<T extends MaybeElement = MaybeElement> = T extends VueInstance ? Exclude<MaybeElement, VueInstance> : T | undefined逐层解读这套类型设计:
| 类型别名 | 含义 | 典型取值 |
|---|---|---|
VueInstance | Vue 组件公开实例 | ComponentPublicInstance |
MaybeElement | 可能成为元素的值的并集 | HTMLElement \| SVGElement \| VueInstance \| undefined \| null |
MaybeElementRef | 元素或元素 ref | MaybeRef<T>,即T \| Ref<T> |
MaybeComputedElementRef | 元素、ref 或 getter | MaybeRefOrGetter<T>,即T \| Ref<T> \| (() => T) |
MaybeComputedElementRefOrArray | 单个、数组或返回数组的 getter | 供需要同时监听多个元素的函数使用 |
UnRefElementReturn | 返回值类型 | 输入为组件实例时剥离VueInstance,否则保留原类型并允许undefined |
值得注意UnRefElementReturn的巧思:当泛型T被推断为VueInstance时,返回类型会被收敛为Exclude<MaybeElement, VueInstance>(即HTMLElement | SVGElement | undefined | null),从类型层面保证"函数永远不返回组件实例"这一契约;当T是普通元素类型时,返回T | undefined。
边界行为:由测试用例验证的 5 种场景
unrefElement的边界行为在 packages/core/unrefElement/index.browser.test.ts 中有完整的浏览器端测试覆盖,逐条印证了其设计意图:
| 测试场景 | 输入 | 期望输出 |
|---|---|---|
| 空值透传 | null/undefined | 原样返回null/undefined |
| 原生元素 ref | useTemplateRef<HTMLElement>('target-node')(绑定<div>) | 返回该HTMLDivElement实例,textContent为'Node 2' |
| 组件实例 | 绑定多根结构组件(根节点为<div id="child-root-node">) | 返回$el,即根节点child-root-node |
| 纯文本组件 | 模板为'This is a text node'的组件 | 返回文本节点(nodeType为Node.TEXT_NODE) |
| 多根组件 | 模板含两个并列根<div>的组件 | 返回空的占位文本节点(Text实例,textContent为空) |
最后两个场景尤其值得开发者警惕:
- Fragment(多根)组件没有单一根元素,Vue 会以注释/占位文本节点作为
$el。因此对多根组件调用unrefElement拿到的是空文本占位节点,而不是某个子元素——若需要具体元素,应改用useTemplateRef精确绑定子节点; - 纯文本根组件的
$el是文本节点,不是HTMLElement,任何依赖element.style之类的 DOM 操作都会失败。
这也解释了UnRefElementReturn为何要保留undefined:在多根组件场景下,返回值可能不具备元素语义。
实战范式:unrefElement 在 VueUse 内部的用法
与其说unrefElement是给终端用户使用的工具,不如说它是 VueUse 整个 DOM 工具链的基石。在 packages/core/index.ts 中它随核心包整体导出,并被 30+ 个核心函数内部引用。从源码结构看,典型调用模式有三类:
1. 作为元素的统一入口:useElementSize
useElementSize 接受MaybeComputedElementRef类型的target,内部先调用unrefElement(target)解析出真实元素,再交给ResizeObserver监听尺寸变化,甚至用它判断目标是否为 SVG 元素:
const isSVG = computed(() => unrefElement(target)?.namespaceURI?.includes('svg')) const width = shallowRef(initialSize.width) const height = shallowRef(initialSize.height)这里unrefElement的价值在于:用户无论传元素 ref、getter 还是组件实例,useElementSize都能一视同仁地拿到可监听的 DOM 节点。
2. 作为当前组件的元素代理:useCurrentElement
useCurrentElement 在onMounted/onUpdated时触发计算,将当前组件实例的$el(或指定的根组件)解析为响应式元素引用:
const currentElement = computedWithControl( () => null, () => (rootComponent ? unrefElement(rootComponent) : vm.proxy!.$el) as E, ) onUpdated(currentElement.trigger) onMounted(currentElement.trigger)它让"在任意时刻获取当前组件根元素"变成可响应式订阅,是 useParentElement 等需要向上追溯 DOM 结构的函数的地基——后者正是通过unrefElement(element)解析后读取el.parentElement。
3. 作为事件绑定的目标解析:onClickOutside
onClickOutside 在注册外部点击监听前,先以unrefElement解析待监听元素,并将解析逻辑与事件监听解耦;当元素的 ref 或 getter 发生变化时,只需重新解析即可切换监听目标,无需感知传入方究竟是元素还是组件。
可以推断:上述模式在 useElementBounding、useElementVisibility、useEventListener、useFocus、useScroll 等函数中反复出现,形成了 VueUse "先unrefElement归一化元素、再执行 DOM 操作"的统一范式。如果你要编写自定义 DOM composable,直接复用unrefElement而非手写$el判断,是保持与 VueUse 生态一致的推荐做法。
实战建议与注意事项
综合文档、源码与测试,使用unrefElement时有几点实战经验:
- 在正确的生命周期使用:挂载前元素 ref 与
$el均为空,unrefElement会原样返回undefined。请像官方示例一样在onMounted之后、或通过watch/whenever观察 ref 变化后再解析; - 对组件 ref 的返回值降低预期:组件根节点可能是文本节点或空占位节点(多根组件),拿到后先做
instanceof HTMLElement之类的守卫再执行 DOM API; - getter 输入适合动态目标:需要跟随选择器或动态条件变化的元素时,可传入
() => document.querySelector(...)形式的 getter,unrefElement每次调用都会重新求值; - 类型注解让代码更安全:为模板 ref 声明明确的泛型(如
useTemplateRef<HTMLElement>('target-node')),可让unrefElement的返回值推断更精确。
总结
unrefElement以一行核心逻辑,统一了 Vue 3 中"元素 ref、组件实例、getter 目标"三种 DOM 获取路径:toValue负责解包,$el探测负责从组件实例穿透到真实节点。配合完善的类型体系与浏览器测试,它既是独立可用的工具函数,更是 VueUse 30+ 个 DOM composable 得以复用同一套元素解析逻辑的底层支柱。理解它,你就掌握了 VueUse DOM 工具链的第一块拼图。
- 前端
【免费下载链接】vueuse
Collection of essential Vue Composition Utilities for Vue 3
相关推荐
VueUse unrefElement 实战指南:在 Vue 3 中统一获取 DOM 元素与组件根节点
VueUse unrefElement 实战指南:在 Vue 3 中统一获取 DOM 元素与组件根节点 unrefElement 是 VueUse 提供的一个工
AI 应用人工智能大模型数字人AI Agent语音前端后端桌面应用移动开发即时通讯3D渲染airi 项目实战:VueUse useCurrentElement 获取组件根元素并驱动响应式 DOM 操作
airi 项目实战:VueUse useCurrentElement 获取组件根元素并驱动响应式 DOM 操作 useCurrentElement 是 VueU
AI 应用人工智能大模型数字人AI Agent语音前端后端桌面应用移动开发即时通讯3D渲染JimuReport 扩展开发指南:2 个扩展接口打通权限控制与自定义字典
JimuReport 扩展开发指南:2 个扩展接口打通权限控制与自定义字典 上周把 JimuReport 交给财务组,新同事第一句话是:为什么我能看到报表设计器
后端数据可视化低代码AI 应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考