Vue 3.5/3.6 新特性深入
Vapor Mode、响应式 Props 解构、useId、Lazy Hydration、性能优化
Vue 3.5 核心更新(2024.9)
一、响应式系统优化
1.1 内存优化(减少 56%)
优化点:
- 重构依赖追踪数据结构
- 减少闭包创建
- 优化 effect 对象复用
性能对比:
typescript
// 创建 10000 个响应式对象
const objects = Array.from({ length: 10000 }, () =>
reactive({ count: 0, name: 'test' })
)
// Vue 3.4: ~45MB
// Vue 3.5: ~20MB (减少 56%)1.2 响应式 Props 解构
新特性:解构 props 时保持响应式。
vue
<script setup lang="ts">
// Vue 3.4 之前 - 失去响应式
const { msg, count } = defineProps<{
msg: string
count: number
}>()
watchEffect(() => {
console.log(msg) // ❌ 不是响应式的
})
// Vue 3.5 - 保持响应式
const { msg, count = 0 } = defineProps<{
msg: string
count?: number
}>()
watchEffect(() => {
console.log(msg) // ✅ 响应式
})
// 支持默认值
const { title = 'Default Title' } = defineProps<{
title?: string
}>()
</script>实现原理:
typescript
// 编译后
const props = defineProps<{ msg: string }>()
// 解构变量被转换为 getter
const msg = computed(() => props.msg)
// 传递给 composable 需要包装为 getter
useMyComposable(() => msg.value)注意事项:
vue
<script setup>
const { count } = defineProps<{ count: number }>()
// ✅ 直接使用
console.log(count)
// ✅ 在模板中使用
// <div>{{ count }}</div>
// ❌ 传递给 composable 需要包装
useCounter(count) // 错误:传递的是值,不是响应式引用
// ✅ 正确做法
useCounter(() => count) // 传递 getter
</script>1.3 onWatcherCleanup
新 API:在 watch/watchEffect 中注册清理函数。
typescript
import { watch, onWatcherCleanup } from 'vue'
watch(id, (newId) => {
const controller = new AbortController()
// 注册清理函数
onWatcherCleanup(() => {
controller.abort()
})
fetch(`/api/data/${newId}`, { signal: controller.signal })
.then(res => res.json())
.then(data => {
// 处理数据
})
})对比旧方法:
typescript
// Vue 3.4 - 使用 onCleanup 参数
watch(id, (newId, oldId, onCleanup) => {
const controller = new AbortController()
onCleanup(() => {
controller.abort()
})
fetch(`/api/data/${newId}`, { signal: controller.signal })
})
// Vue 3.5 - 使用 onWatcherCleanup(更灵活)
watch(id, async (newId) => {
const controller = new AbortController()
onWatcherCleanup(() => {
controller.abort()
})
// 可以在任何地方调用,包括异步函数内部
const data = await fetch(`/api/data/${newId}`, {
signal: controller.signal
})
})1.4 深层响应式优化
大数组性能提升 10 倍:
typescript
// 创建大型深层数组
const largeArray = reactive(
Array.from({ length: 10000 }, (_, i) => ({
id: i,
nested: { value: i }
}))
)
// Vue 3.4: 修改单个元素耗时 ~50ms
// Vue 3.5: 修改单个元素耗时 ~5ms
largeArray[5000].nested.value = 999二、新增 API
2.1 useId()
作用:生成 SSR 安全的唯一 ID。
vue
<script setup>
import { useId } from 'vue'
const id = useId()
</script>
<template>
<label :for="id">Username</label>
<input :id="id" type="text" />
</template>SSR 安全性:
typescript
// 服务端渲染
const id1 = useId() // 'v-0'
const id2 = useId() // 'v-1'
// 客户端 hydration
const id1 = useId() // 'v-0' (相同)
const id2 = useId() // 'v-1' (相同)
// 避免 hydration mismatch对比随机 ID:
vue
<!-- ❌ 不安全 - SSR 和客户端不一致 -->
<script setup>
const id = Math.random().toString(36)
</script>
<!-- ✅ 安全 - SSR 和客户端一致 -->
<script setup>
const id = useId()
</script>2.2 useTemplateRef()
作用:类型安全的模板引用。
vue
<script setup lang="ts">
import { useTemplateRef, onMounted } from 'vue'
// 自动推导类型
const inputRef = useTemplateRef<HTMLInputElement>('input')
onMounted(() => {
inputRef.value?.focus() // 类型安全
})
</script>
<template>
<input ref="input" />
</template>对比旧方法:
vue
<!-- Vue 3.4 -->
<script setup lang="ts">
const inputRef = ref<HTMLInputElement>()
onMounted(() => {
inputRef.value?.focus()
})
</script>
<template>
<input ref="inputRef" />
</template>
<!-- Vue 3.5 -->
<script setup lang="ts">
const inputRef = useTemplateRef<HTMLInputElement>('input')
onMounted(() => {
inputRef.value?.focus()
})
</script>
<template>
<input ref="input" />
</template>2.3 Teleport defer
延迟 teleport:等待目标元素挂载后再传送。
vue
<template>
<Teleport to="#modal-container" defer>
<div class="modal">Modal content</div>
</Teleport>
</template>
<!-- 即使 #modal-container 在后面定义也能正常工作 -->
<div id="modal-container"></div>使用场景:
vue
<!-- App.vue -->
<template>
<RouterView />
<div id="modals"></div>
</template>
<!-- SomePage.vue -->
<template>
<!-- defer 确保 #modals 已挂载 -->
<Teleport to="#modals" defer>
<Modal />
</Teleport>
</template>三、SSR 改进
3.1 Lazy Hydration
作用:延迟非关键组件的 hydration,提升首屏性能。
vue
<script setup>
import { defineAsyncComponent } from 'vue'
// 懒加载 + 懒 hydration
const HeavyComponent = defineAsyncComponent({
loader: () => import('./HeavyComponent.vue'),
hydrate: 'lazy' // 延迟 hydration
})
</script>
<template>
<div>
<CriticalContent />
<HeavyComponent /> <!-- 滚动到可见区域时才 hydrate -->
</div>
</template>Hydration 策略:
typescript
// 1. 立即 hydration(默认)
const Component = defineAsyncComponent({
loader: () => import('./Component.vue')
})
// 2. 懒 hydration(可见时)
const Component = defineAsyncComponent({
loader: () => import('./Component.vue'),
hydrate: 'lazy'
})
// 3. 交互时 hydration
const Component = defineAsyncComponent({
loader: () => import('./Component.vue'),
hydrate: 'interaction'
})
// 4. 媒体查询 hydration
const Component = defineAsyncComponent({
loader: () => import('./Component.vue'),
hydrate: 'media',
media: '(min-width: 768px)'
})3.2 Hydration Mismatch 改进
更好的错误提示:
Vue 3.4:
Hydration mismatch
Vue 3.5:
Hydration mismatch in <div>:
- Server rendered: <div>Server</div>
- Client expected: <div>Client</div>
at <App>自动恢复:
vue
<!-- 服务端渲染 -->
<div>{{ serverData }}</div>
<!-- 客户端期望 -->
<div>{{ clientData }}</div>
<!-- Vue 3.5 会自动恢复,并给出警告 -->四、开发体验改进
4.1 自定义元素支持增强
typescript
// vite.config.ts
export default {
plugins: [
vue({
template: {
compilerOptions: {
isCustomElement: (tag) => tag.startsWith('my-')
}
}
})
]
}vue
<template>
<!-- 不会被当作 Vue 组件 -->
<my-custom-element></my-custom-element>
</template>4.2 更好的 TypeScript 支持
泛型组件:
vue
<script setup lang="ts" generic="T">
defineProps<{
items: T[]
renderItem: (item: T) => string
}>()
</script>
<template>
<div v-for="item in items" :key="item">
{{ renderItem(item) }}
</div>
</template>使用:
vue
<template>
<GenericList
:items="users"
:render-item="(user) => user.name"
/>
<!-- user 自动推导为 User 类型 -->
</template>Vue 3.6 展望(Vapor Mode)
一、Vapor Mode 概述
定义:无虚拟 DOM 的编译模式,性能接近 Solid.js。
核心理念:
- 编译时优化:将模板编译为细粒度的 DOM 操作
- 无虚拟 DOM:直接操作真实 DOM
- 响应式驱动:响应式数据变化直接更新 DOM
性能对比:
传统模式(Virtual DOM):
数据变化 → 生成新 VNode → Diff → 更新 DOM
Vapor Mode:
数据变化 → 直接更新 DOM二、Vapor Mode 示例
输入模板:
vue
<template>
<div>
<h1>{{ title }}</h1>
<p>Static text</p>
<button @click="increment">{{ count }}</button>
</div>
</template>
<script setup>
const title = ref('Hello')
const count = ref(0)
const increment = () => count.value++
</script>传统编译输出(简化):
javascript
function render() {
return h('div', [
h('h1', title.value),
h('p', 'Static text'),
h('button', { onClick: increment }, count.value)
])
}
// 每次更新都需要:
// 1. 生成新 VNode
// 2. Diff 对比
// 3. 更新 DOMVapor Mode 编译输出(简化):
javascript
function setup() {
const div = document.createElement('div')
const h1 = document.createElement('h1')
const p = document.createElement('p')
const button = document.createElement('button')
p.textContent = 'Static text'
div.appendChild(h1)
div.appendChild(p)
div.appendChild(button)
// 细粒度响应式更新
effect(() => {
h1.textContent = title.value
})
effect(() => {
button.textContent = count.value
})
button.addEventListener('click', increment)
return div
}
// 更新时:
// 直接修改 h1.textContent 或 button.textContent
// 无需 VNode,无需 Diff三、Vapor Mode 优势
性能提升:
- 初始渲染:快 20-30%
- 更新性能:快 50-100%
- 内存使用:减少 40-60%
适用场景:
- 大型列表
- 频繁更新的组件
- 性能敏感的应用
限制:
- 不支持某些动态特性(如动态组件)
- 需要编译时确定结构
- 与现有生态兼容性需要验证
四、Vapor Mode 使用
启用方式(预计):
typescript
// vite.config.ts
export default {
plugins: [
vue({
vapor: true // 启用 Vapor Mode
})
]
}或按组件启用:
vue
<script vapor>
// 该组件使用 Vapor Mode
import { ref } from 'vue'
const count = ref(0)
</script>面试高频题
1: Vue 3.5 响应式 Props 解构的原理是什么?
答案:
编译时将解构变量转换为 computed getter:
typescript
// 源码
const { msg } = defineProps<{ msg: string }>()
// 编译后
const props = defineProps<{ msg: string }>()
const msg = computed(() => props.msg)注意:传递给 composable 需要包装为 getter。
2: useId() 解决了什么问题?
答案:
问题:SSR 时,服务端和客户端生成的随机 ID 不一致,导致 hydration mismatch。
解决:
- 服务端和客户端使用相同的计数器
- 保证 ID 一致性
- 避免 hydration 错误
3: onWatcherCleanup 相比旧的 onCleanup 有什么优势?
答案:
优势:
- 可以在异步函数中调用
- 不依赖回调参数
- 更灵活的清理时机
typescript
// 旧方法 - 不能在 async 中使用
watch(id, async (newId, oldId, onCleanup) => {
onCleanup(() => {}) // ❌ 在 async 中无效
})
// 新方法 - 可以在 async 中使用
watch(id, async (newId) => {
onWatcherCleanup(() => {}) // ✅ 正常工作
})4: Lazy Hydration 如何提升性能?
答案:
原理:
- 延迟非关键组件的 hydration
- 只 hydrate 可见区域的组件
- 减少首屏 JavaScript 执行时间
效果:
- 首屏性能提升 30-50%
- TTI(可交互时间)缩短
- 更好的用户体验
5: Vapor Mode 和传统模式有什么区别?
答案:
| 特性 | 传统模式 | Vapor Mode |
|---|---|---|
| 虚拟 DOM | ✅ 使用 | ❌ 不使用 |
| Diff 算法 | ✅ 需要 | ❌ 不需要 |
| 更新方式 | 重新渲染 + Diff | 直接更新 DOM |
| 性能 | 基准 | 快 50-100% |
| 内存 | 基准 | 减少 40-60% |
核心:Vapor Mode 通过编译时优化,将响应式更新直接映射到 DOM 操作。
6: Vue 3.5 的内存优化是如何实现的?
答案:
优化点:
- 重构依赖追踪数据结构
- 减少闭包创建
- 优化 effect 对象复用
- 改进垃圾回收策略
效果:内存使用减少 56%。
7: 响应式 Props 解构有什么限制?
答案:
限制:
- 传递给 composable 需要包装为 getter
- 不能直接赋值给其他变量
- 解构后的变量是 computed,不是 ref
typescript
const { count } = defineProps<{ count: number }>()
// ❌ 错误
useCounter(count)
// ✅ 正确
useCounter(() => count)
// ❌ 错误
const myCount = count
// ✅ 正确
const myCount = computed(() => count)8: Teleport defer 解决了什么问题?
答案:
问题:Teleport 目标元素可能还未挂载。
vue
<!-- ❌ 错误 - #modal 还不存在 -->
<Teleport to="#modal">
<div>Content</div>
</Teleport>
<div id="modal"></div>
<!-- ✅ 正确 - defer 等待目标挂载 -->
<Teleport to="#modal" defer>
<div>Content</div>
</Teleport>
<div id="modal"></div>9: Vue 3.5 如何优化大数组性能?
答案:
优化:
- 特殊处理大型深层数组
- 优化依赖收集策略
- 减少不必要的响应式转换
效果:大数组修改性能提升 10 倍。
10: 什么时候应该使用 Vapor Mode?
答案:
适用场景:
- 大型列表渲染
- 频繁更新的组件
- 性能敏感的应用
- 移动端应用
不适用场景:
- 需要动态组件
- 复杂的条件渲染
- 高度动态的结构
建议:等 Vue 3.6 正式发布后,根据实际需求选择性启用。