☰
VueUse unrefElement 完全指南:从 Vue ref 与组件实例中获取真实 DOM 元素
2026/10/1 2:09:29 网站建设 项目流程
  • 前端

【免费下载链接】vueuse

Collection of essential Vue Composition Utilities for Vue 3

项目地址:https://gitcode.com/gh_mirrors/vu/vueuse
点击查看免费下载

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 }

整个解析过程只有两步:

  1. toValue(elRef)解包:toValue是 Vue 3.3+ 提供的统一取值函数,能同时处理普通值、ref、getter 函数三种输入形态,等价于旧写法isRef(elRef) ? elRef.value : typeof elRef === 'function' ? elRef() : elRef。这意味着你可以直接传入useTemplateRef('div')得到的 ref,也可以传入() => document.querySelector('#app')这样的 getter;
  2. $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

逐层解读这套类型设计:

类型别名含义典型取值
VueInstanceVue 组件公开实例ComponentPublicInstance
MaybeElement可能成为元素的值的并集HTMLElement \| SVGElement \| VueInstance \| undefined \| null
MaybeElementRef元素或元素 refMaybeRef<T>,即T \| Ref<T>
MaybeComputedElementRef元素、ref 或 getterMaybeRefOrGetter<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
原生元素 refuseTemplateRef<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时有几点实战经验:

  1. 在正确的生命周期使用:挂载前元素 ref 与$el均为空,unrefElement会原样返回undefined。请像官方示例一样在onMounted之后、或通过watch/whenever观察 ref 变化后再解析;
  2. 对组件 ref 的返回值降低预期:组件根节点可能是文本节点或空占位节点(多根组件),拿到后先做instanceof HTMLElement之类的守卫再执行 DOM API;
  3. getter 输入适合动态目标:需要跟随选择器或动态条件变化的元素时,可传入() => document.querySelector(...)形式的 getter,unrefElement每次调用都会重新求值;
  4. 类型注解让代码更安全:为模板 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

项目地址:https://gitcode.com/gh_mirrors/vu/vueuse
点击查看免费下载

相关推荐

上一篇:awesome-design-md 复古特辑:如何还原 2001 年任天堂 Y2K 控制台镀铬风格,一篇全解析
下一篇:Goose安装教程:零基础10分钟装好本地AI智能体

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询