Vue 3.5+ 新特性在 Nuxt4 中的应用

Nuxt4 基于 Vue 3.5+,这意味着你可以使用 Vue 最新的语言特性。这些特性不是华而不实的语法糖——它们解决了 Vue 3 早期版本中的真实痛点:useTemplateRef 解决了模板引用的类型安全问题,Reactive Props Destructure 消除了 toRefs 的样板代码,useId 解决了 SSR 的 ID 不匹配问题。本章逐一拆解这些特性的设计动机和在 Nuxt4 SSR 场景下的最佳实践。

1. useTemplateRef

1.1 问题:模板引用的类型黑洞

Vue 3.4 及之前,模板引用通过同名变量绑定:

<template>
  <input ref="inputRef" />
</template>
 
<script setup>
const inputRef = ref(null)  // 类型是 Ref<null>,不知道是 HTMLInputElement
</script>

问题:

  • inputRef 的类型是 Ref&lt;null&gt;,TypeScript 不知道它会绑定到什么元素
  • 如果模板中的 ref="inputRef" 拼写错误,没有任何报错
  • 无法区分"变量同名但不是模板引用"的情况

1.2 useTemplateRef 的解法

<template>
  <input ref="input" />
  <video ref="player" />
</template>
 
<script setup>
const inputEl = useTemplateRef<HTMLInputElement>('input')
const playerEl = useTemplateRef<HTMLVideoElement>('player')
 
onMounted(() => {
  inputEl.value?.focus()           // ✅ 自动补全 HTMLInputElement 方法
  playerEl.value?.play()           // ✅ 自动补全 HTMLVideoElement 方法
})
</script>

优势:

  • 类型安全:泛型明确指定元素类型,IDE 自动补全
  • 解耦命名:模板中的 ref="input" 和脚本中的 inputEl 可以不同名
  • 语义清晰useTemplateRef 明确表示"这是模板引用",不会和普通 ref() 混淆

1.3 在 Nuxt4 中的 SSR 注意事项

模板引用在 SSR 阶段始终是 null——服务端没有 DOM。所有通过 useTemplateRef 获取的元素操作必须在 onMounted 或客户端生命周期中执行:

const canvasEl = useTemplateRef<HTMLCanvasElement>('canvas')
 
// ❌ SSR 阶段 canvasEl.value 是 null
// canvasEl.value.getContext('2d')
 
// ✅ 在 onMounted 中使用
onMounted(() => {
  const ctx = canvasEl.value?.getContext('2d')
})

2. Reactive Props Destructure

2.1 问题:toRefs 的样板代码

Vue 3.4 及之前,从 props 中解构会丢失响应性

<script setup>
const props = defineProps<{ title: string; count: number }>()
 
// ❌ 解构后丢失响应性
const { title, count } = props  // title 和 count 是普通值,不会更新
 
// ✅ 需要 toRefs 保持响应性
const { title, count } = toRefs(props)
</script>

每个需要解构 props 的组件都要写 toRefs(props),纯粹的样板代码。

2.2 Vue 3.5 的解法

Vue 3.5 开启了 Reactive Props Destructure——直接解构 defineProps 即保持响应性:

<script setup>
const { title, count = 0 } = defineProps<{
  title: string
  count?: number
}>()
 
// ✅ title 和 count 保持响应性
// ✅ count 有默认值 0
// ✅ 不需要 toRefs,不需要 withDefaults
 
watchEffect(() => {
  console.log(title, count)  // 父组件更新 props 时会重新执行
})
</script>

关键变化:

  • 直接解构const &#123; title &#125; = defineProps&lt;...&gt;() 保持响应性
  • 默认值内联count = 0 替代 withDefaults
  • 编译器魔法:Vue 编译器在编译阶段自动将解构转换为响应式访问

2.3 与 withDefaults 的对比

<!-- Vue 3.4:withDefaults -->
<script setup>
const props = withDefaults(defineProps<{
  title: string
  count?: number
  items?: string[]
}>(), {
  count: 0,
  items: () => [],
})
</script>
 
<!-- Vue 3.5:直接解构 -->
<script setup>
const {
  title,
  count = 0,
  items = [],
} = defineProps<{
  title: string
  count?: number
  items?: string[]
}>()
</script>

更简洁,更符合 JavaScript 的直觉。注意:引用类型默认值(数组、对象)不再需要用工厂函数包裹——编译器自动处理。

3. useId

3.1 问题:SSR ID 不匹配

在 SSR 场景中,组件在服务端和客户端分别渲染一次。如果组件内生成随机 ID(如 Math.random()),两次渲染的 ID 不一致,会导致 Hydration Mismatch 警告:

<script setup>
const id = `input-${Math.random().toString(36).slice(2)}`
// 服务端:input-abc123
// 客户端:input-xyz789
// → Hydration Mismatch!
</script>
 
<template>
  <label :for="id">Name</label>
  <input :id="id" />
</template>

3.2 useId 的解法

<script setup>
const id = useId()
// 服务端和客户端生成相同的确定性 ID
</script>
 
<template>
  <label :for="id">Name</label>
  <input :id="id" />
</template>

useId() 生成的 ID 在服务端和客户端完全一致(基于组件树的位置而非随机数)。这在以下场景特别重要:

  • 表单组件&lt;label for=""&gt;&lt;input id=""&gt; 需要匹配
  • 无障碍属性aria-describedbyaria-labelledby 引用的 ID
  • 可复用组件:同一个组件在页面上多次使用,每个实例需要唯一 ID

3.3 在 Nuxt4 中的应用

<!-- components/FormField.vue -->
<script setup>
const { label, error } = defineProps<{
  label: string
  error?: string
}>()
 
const inputId = useId()
const errorId = useId()
</script>
 
<template>
  <div>
    <label :for="inputId">{{ label }}</label>
    <slot :id="inputId" :aria-describedby="error ? errorId : undefined" />
    <p v-if="error" :id="errorId" class="text-error text-sm">{{ error }}</p>
  </div>
</template>

4. onWatcherCleanup

4.1 问题:异步 watcher 的竞态条件

<script setup>
const userId = ref('1')
 
watch(userId, async (id) => {
  const data = await fetchUser(id)
  // 如果 userId 在 fetchUser 期间再次变化
  // 这里设置的是旧请求的结果!
  userData.value = data
})
</script>

用户快速切换 ID 时:请求 A(ID=1)发出 → 请求 B(ID=2)发出 → 请求 B 先返回 → 请求 A 后返回 → 显示 ID=1 的数据(错误!)

4.2 onWatcherCleanup 的解法

<script setup>
watch(userId, async (id) => {
  let cancelled = false
  onWatcherCleanup(() => { cancelled = true })
 
  const data = await fetchUser(id)
  if (!cancelled) {
    userData.value = data
  }
})
</script>

onWatcherCleanup 注册一个清理函数,在 watcher 重新执行(源值变化)或组件卸载时调用。这是处理异步 watcher 竞态条件的官方方案。

4.3 配合 AbortController

更彻底的方案是取消正在进行的网络请求:

<script setup>
watch(userId, async (id) => {
  const controller = new AbortController()
  onWatcherCleanup(() => controller.abort())
 
  try {
    const data = await $fetch(`/api/users/${id}`, {
      signal: controller.signal,
    })
    userData.value = data
  } catch (e) {
    if (e.name !== 'AbortError') throw e
  }
})
</script>

5. 响应式系统优化

5.1 Reactivity 内存优化

Vue 3.5 对响应式系统做了底层重构,核心变化:

  • 内存减少 56%refreactive 的内部数据结构优化,大型应用(数千个响应式对象)内存占用显著降低
  • Dependency tracking 重写:使用双向链表替代 Set,减少内存分配
  • 响应式数组优化:大数组操作(push、splice)性能提升

这些优化对开发者是透明的——不需要修改任何代码,升级到 Vue 3.5 / Nuxt4 自动生效。

5.2 defineModel 稳定

defineModel 在 Vue 3.4 中还是实验性特性,Vue 3.5 正式稳定:

<!-- 子组件 -->
<script setup>
const modelValue = defineModel<string>()
const count = defineModel<number>('count')
</script>
 
<template>
  <input v-model="modelValue" />
  <button @click="count++">{{ count }}</button>
</template>
 
<!-- 父组件 -->
<template>
  <MyInput v-model="name" v-model:count="counter" />
</template>

defineModeldefineProps + defineEmits 的语法糖,自动生成 modelValue prop 和 update:modelValue emit。

5.3 Deferred Teleport

Vue 3.5 的 &lt;Teleport&gt; 支持 defer 属性——延迟传送直到目标元素挂载:

<template>
  <Teleport to="#modals" defer>
    <Modal v-if="showModal" />
  </Teleport>
 
  <!-- 目标容器可能在 Teleport 之后渲染 -->
  <div id="modals" />
</template>

在 Nuxt4 的 SSR 场景中,defer 避免了目标元素尚未渲染时 Teleport 报错的问题。

本章小结

  • useTemplateRef:类型安全的模板引用,泛型指定元素类型,SSR 阶段始终为 null
  • Reactive Props Destructure:直接解构 defineProps 保持响应性,替代 toRefswithDefaults
  • useId:SSR 安全的确定性 ID 生成,解决 Hydration Mismatch,用于表单和无障碍属性
  • onWatcherCleanup:异步 watcher 的清理机制,配合 AbortController 解决竞态条件
  • 响应式优化:内存减少 56%,defineModel 稳定,Deferred Teleport 兼容 SSR