WSの小屋

Vue 3.5 新特性全解:useTemplateRef、useId 与性能优化

前言

Vue 3.5(代号 "Tengen Toppa Gurren Lagann")于 2024 年 9 月正式发布,带来了多项重要改进。到 2025 年中,Vue 3.5 已经成为生产环境的主流版本。这个版本在响应式系统、内存管理、SSR 体验和开发效率方面都有显著提升。

本文将全面解析 Vue 3.5 的重要新特性,包括 useTemplateRefuseId、Reactive Props Destructure、延迟 Teleport、内存优化等。


一、useTemplateRef:更安全的模板引用

1.1 旧方案的痛点

在 Vue 3.5 之前,获取模板引用需要通过 ref attribute,变量名必须与模板中的 ref="inputEl" 字符串一致,编译器不做检查,重命名后容易出错。

1.2 useTemplateRef 的优雅方案

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

const inputEl = useTemplateRef<HTMLInputElement>('inputEl')

onMounted(() => {
  inputEl.value?.focus()
})
</script>
<template>
  <input ref="inputEl" />
</template>
特性 旧方案 (ref) 新方案 (useTemplateRef)
类型推导 需手动标注 泛型自动推导
初始值 需要 null 自动处理
可写性 可写(不安全) 只读(更安全)
语义 通用 ref 明确表示模板引用

1.3 实战:组合式表单组件

<script setup lang="ts">
import { useTemplateRef, ref } from 'vue'

interface FormExpose {
  validate: () => Promise<boolean>
  reset: () => void
  submit: () => void
}

const formRef = useTemplateRef<FormExpose>('formRef')
const submitBtnRef = useTemplateRef<HTMLButtonElement>('submitBtn')
const loading = ref(false)

async function handleSubmit() {
  const valid = await formRef.value?.validate()
  if (valid) {
    loading.value = true
    formRef.value?.submit()
  }
}

defineExpose({ focus: () => submitBtnRef.value?.focus() })
</script>

二、useId:可访问性与 SSR 一致性

2.1 问题背景

Math.random() 生成 ID 在 SSR 场景会导致 hydration mismatch——服务端和客户端生成的 ID 不同。

2.2 useId 解决方案

<script setup lang="ts">
import { useId } from 'vue'

const inputId = useId()
const labelId = useId()
const errorId = useId()
</script>
<template>
  <div class="form-field">
    <label :id="labelId" :for="inputId">名称</label>
    <input :id="inputId" :aria-labelledby="labelId" :aria-describedby="errorId" type="text" />
    <span :id="errorId" class="error">错误信息</span>
  </div>
</template>

2.3 原理

useId 基于 Vue 应用的层级结构和组件实例生成 ID,确保同一组件在 SSR 和 CSR 中生成相同 ID。生成的 ID 类似 v-0-0v-0-1,前缀可通过 app.config.idPrefix 配置。


三、Reactive Props Destructure(响应式 Props 解构)

3.1 背景与动机

在 Vue 3.4 及以前,解构 defineProps 的值会丢失响应性。Vue 3.5 编译器自动为解构的 props 添加响应性追踪。

<script setup lang="ts">
// ✅ Vue 3.5:解构后仍然保持响应性
const { count, title } = defineProps<{
  count: number
  title: string
}>()

// ✅ watch 可以直接接收解构的值
watch(count, (newVal) => {
  console.log('count changed:', newVal)
})

// ✅ 默认值语法
const { count = 0, title = 'Default', items = [] } = defineProps<{
  count?: number
  title?: string
  items?: string[]
}>()
</script>

编译后近似于 toRefs

const __props = defineProps<{ count: number; title: string }>()
const count = __toRef(__props, 'count')
const title = __toRef(__props, 'title')

四、延迟 Teleport(Deferred Teleport)

Vue 3.5 为 <Teleport> 增加了 defer 属性,让 Teleport 在渲染周期结束后再进行传送,避免目标元素还未渲染的时序问题:

<template>
  <Teleport defer to="#modal-container">
    <div class="modal">内容</div>
  </Teleport>
</template>

五、响应式系统内存优化

Vue 3.5 对响应式系统进行了重大重构,减少了约 56% 的内存占用

优化原理

Vue 3.4 中每个响应式依赖关系都会创建一个 Dep 对象(大量小对象 → GC 压力大)。Vue 3.5 使用 双向链表 + 版本号 替代了对象数组:

优化 说明 收益
双向链表 替代数组存储依赖 更快的增删操作
版本号 用版本号替代脏标记 减少不必要的计算
链表节点复用 减少对象创建 降低 GC 压力
批量清理 统一清理失效依赖 减少重排序开销

性能对比

// 10,000 个响应式数据的场景
// Vue 3.4: 内存 ~15MB, 初始渲染 ~120ms, 更新渲染 ~25ms
// Vue 3.5: 内存 ~6.5MB (-56%), 初始渲染 ~75ms (-37%), 更新渲染 ~12ms (-52%)

大列表优化示例

<script setup lang="ts">
import { shallowRef } from 'vue'
import { useVirtualList } from '@vueuse/core'

// ✅ 大列表使用 shallowRef
const items = shallowRef(
  Array.from({ length: 10000 }, (_, i) => ({ id: i, name: `Item ${i}` }))
)

const { list, containerProps, wrapperProps } = useVirtualList(items, { itemHeight: 40 })
</script>

六、onWatcherCleanup

import { watch, onWatcherCleanup } from 'vue'

watch(id, (newId) => {
  const controller = new AbortController()
  fetch(`/api/data/${newId}`, { signal: controller.signal })
  onWatcherCleanup(() => controller.abort())
})

七、迁移指南

npm install vue@^3.5
npm install -D @vitejs/plugin-vue@^5.1 vue-tsc@^2.1

迁移检查清单

项目 动作 优先级
Template refs 逐步替换为 useTemplateRef
Props 解构 移除 withDefaults,使用解构默认值
ID 生成 替换 Math.randomuseId 高(SSR)
Teleport 检查是否需要添加 defer
Watch 清理 迁移到 onWatcherCleanup
实验性配置 移除 reactivePropDestructure 配置

渐进式迁移示例

<!-- 迁移后 (Vue 3.5) -->
<script setup lang="ts">
import { useTemplateRef, useId, watch, onWatcherCleanup } from 'vue'

const { count = 0, label = 'Default' } = defineProps<{
  count?: number
  label?: string
}>()

const inputRef = useTemplateRef<HTMLInputElement>('inputRef')
const inputId = useId()

watch(count, (newVal) => {
  const timer = setTimeout(() => console.log(newVal), 100)
  onWatcherCleanup(() => clearTimeout(timer))
})
</script>

八、性能优化最佳实践

<template>
  <!-- 静态内容使用 v-once -->
  <header v-once><h1>{{ appTitle }}</h1></header>

  <!-- 大列表项使用 v-memo -->
  <div v-for="item in items" :key="item.id" v-memo="[item.id, item.updated]">
    {{ item.name }}
  </div>
</template>

结语

Vue 3.5 是一个重要的里程碑版本。响应式系统的内存优化让大型应用的性能有了质的飞跃,useTemplateRefuseId 解决了开发中的常见痛点,Reactive Props Destructure 让代码更加简洁优雅。Vue 3.6 已在规划中,预计将带来 Vapor Mode(无虚拟 DOM 模式)等重要特性。

Comments | 0条评论