跳转到内容

useTemplateRef 实现原理

刀刀
0字
0分钟
2026/8/16

useTemplateRefVue 3.5新增的 API,用于获取模板中的 DOM 元素或组件实例。

在 3.5 之前,获取模板中的DOM元素或组件实例通过 ref 属性和 ref 方法实现。但这种方式需要在模板中添加额外的 ref 属性,且要求二者名字一致。

vue
<script setup>
import { ref } from 'vue'
const elRef = ref(null)
</script>

<template>
  <div ref="elRef">Hello World</div>
</template>

useTemplateRef 避免了这个问题,允许模板中使用任意名字的 ref 属性,在组件中通过 useTemplateRef 函数获取对应的DOM元素或组件实例。

vue
<script setup>
import { useTemplateRef } from 'vue'
const containerRef = useTemplateRef('elRef')
</script>

<template>
  <div ref="elRef">Hello World</div>
</template>

用法类似 document.querySelector

ref和useTemplateRef

实现

思路

搭建一个空函数,接收字符串 key,返回 ref 对象。

js
export function useTemplateRef(key) {
  const container = shallowRef()

  return container
}

如何获取 key 对应的 ref 对象?

Vue2中,获取 ref 组件实例通过 this.$refs.xxVue3没有 this,但可以通过 getCurrentInstance() 获取当前组件实例。

vue
<script setup>
import { getCurrentInstance } from 'vue'
// const elRef = ref(null) 注意,这里不能声明 ref,否则 vm 的 refs 对象不会再有 elRef
const vm = getCurrentInstance()

console.log(vm)
</script>

<template>
  <div ref="elRef">Hello World</div>
</template>

vm数据截图

Object.defineProperty 劫持 vmrefs 对象,当访问 refs 时返回自定义的 ref 对象。

js
export function useTemplateRef(key) {
  const container = shallowRef()

  const vm = getCurrentInstance() 
  Object.defineProperty(vm.refs, key, {
    get() {}, 
    set(value) {
      console.log('vm set =>', value) 
    }, 
  }) 

  return container
}

报错

控制台报错:

报错截图

提示 refs 属性无法被拓展。查看Vue3源码,refs 属性被 Object.freeze 冻结了。

原因:性能考虑。一个项目中,10 个组件可能只有 5、6 个需要绑定 ref 获取实例,但如果每个组件实例 vm 都绑定一个 refs 对象,即使只是空对象,项目变大后性能开销也会很大。

Vue团队的处理方式:先创建一个冻结对象 Object.freeze({}),新建组件实例时都使用这个默认冻结对象。如果组件内需要声明 ref 获取组件实例,再替换成对应的 ref 对象。

源码借鉴

useTemplateRef 的官方源码,关键部分:

ts
export function useTemplateRef<T = unknown, Keys extends string = string>(
  key: Keys,
): TemplateRef<T> {
  const i = getCurrentInstance() // 获取当前组件实例
  const r = shallowRef(null)
  if (i) {
    const refs = i.refs === EMPTY_OBJ ? (i.refs = {}) : i.refs // 如果当前的组件实例是默认的冻结对象,则声明一个新的对象代替;否则继续复用
    let desc: PropertyDescriptor | undefined
    if (
      __DEV__ &&
      (desc = Object.getOwnPropertyDescriptor(refs, key)) &&
      !desc.configurable
    ) {
      warn(`useTemplateRef('${key}') already exists.`)
    } else {
      Object.defineProperty(refs, key, {
        enumerable: true,
        get: () => r.value,
        set: (val) => (r.value = val),
      })
    }
  } else if (__DEV__) {
    warn(
      `useTemplateRef() is called when there is no active component ` +
        `instance to be associated with.`,
    )
  }
  const ret = __DEV__ ? readonly(r) : r
  if (__DEV__) {
    knownTemplateRefs.add(ret)
  }
  return ret
}

解决方案

仿照源码,获取 vm 实例后手动把 vm.refs 替换为新对象,即可拓展 vm.refs。(这里不用源码的判断,因为框架底层没有导出 EMPTY_OBJ 对象)

set 中把值赋值给 shallowRef 对象;在 get 中返回 shallowRef 对象。这样访问 vm.refs[key] 时,返回的就是自定义的 ref 对象。

js
export function useTemplateRef(key) {
  const container = shallowRef()

  const vm = getCurrentInstance()
  vm.refs = {} // 转为普通的空对象
  Object.defineProperty(vm.refs, key, {
    get() {
      return container.value // 返回自定义的 ref 对象
    },
    set(value) {
      container.value = value // 把值赋值给 shallowRef 响应式变量
    },
  })

  return container
}
vue
<script setup>
import { useTemplateRef } from './hook'

const containerRef = useTemplateRef('elRef')

onMounted(() => {
  console.log(containerRef.value)
})
</script>

<template>
  <div ref="elRef">Hello World</div>
</template>

优化与 Bug 解决

使用两个 useTemplateRef 函数,观察结果。

vue
<script setup lang="ts">
import { onMounted } from 'vue'
import { useTemplateRef } from './hook'

const containerRef = useTemplateRef('elRef')
const containerRef1 = useTemplateRef('aRef')
onMounted(() => {
  console.log(containerRef.value)
  console.log(containerRef1.value)
})
</script>

<template>
  <div ref="elRef">{{ containerRef }}</div>
  <p ref="aRef">{{ containerRef1 }}</p>
</template>

<style scoped></style>

使用两个 useTemplateRef 结果

可以看到,前一个 useTemplateRef 声明的变量变成了 undefined。因为每次调用 useTemplateRef 都会把 vm.refs 赋值为空对象,前一个 useTemplateRef 声明的 ref 对象被覆盖了。

解决这个 Bug:在函数外部声明一个 WeakMap,每次调用 useTemplateRef 时先判断 WeakMap 中是否存在 vm 实例。存在则直接复用;不存在则存入 WeakMap 并继续后续操作。

js
const vmMap = new WeakMap() 

export function useTemplateRef(key) {
  const container = shallowRef()

  const vm = getCurrentInstance()
  if (!vmMap.has(vm)) {
    vm.refs = {} // 转为普通的空对象
    vmMap.set(vm, vm.refs) 
  } 
  Object.defineProperty(vm.refs, key, {
    get() {
      return container.value // 返回自定义的 ref 对象
    },
    set(value) {
      container.value = value // 把值赋值给 shallowRef 响应式变量
    },
  })

  return container
}

完整代码

js
import { shallowRef, getCurrentInstance } from 'vue'

const wmMap = new WeakMap()

export function useTemplateRef(key) {
  const container = shallowRef()

  const vm = getCurrentInstance()
  // 判断是否有vm,有的话就不重新赋值,避免覆盖旧值
  if (!wmMap.has(vm)) {
    vm.refs = {}
    wmMap.set(vm, {})
  }
  Object.defineProperty(vm.refs, key, {
    get() {
      return container.value
    },
    set(value) {
      container.value = value
    },
  })

  return container
}
vue
<script setup lang="ts">
import { onMounted } from 'vue'
import { useTemplateRef } from './hook'

const containerRef = useTemplateRef('elRef')
const containerRef1 = useTemplateRef('aRef')
onMounted(() => {
  console.log(containerRef.value)
  console.log(containerRef1.value)
})
</script>

<template>
  <div ref="elRef">{{ containerRef }}</div>
  <p ref="aRef">{{ containerRef1 }}</p>
</template>

<style scoped></style>

动手实操

跳转预览:点击跳转

贡献者

The avatar of contributor named as duyidao duyidao
The avatar of contributor named as 刀刀 刀刀

页面历史

刀刀博客累计访客 人;文档累计访问量共