Skip to content

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. 更新 DOM

Vapor 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 有什么优势?

答案

优势

  1. 可以在异步函数中调用
  2. 不依赖回调参数
  3. 更灵活的清理时机
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 的内存优化是如何实现的?

答案

优化点

  1. 重构依赖追踪数据结构
  2. 减少闭包创建
  3. 优化 effect 对象复用
  4. 改进垃圾回收策略

效果:内存使用减少 56%。


7: 响应式 Props 解构有什么限制?

答案

限制

  1. 传递给 composable 需要包装为 getter
  2. 不能直接赋值给其他变量
  3. 解构后的变量是 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?

答案

适用场景

  1. 大型列表渲染
  2. 频繁更新的组件
  3. 性能敏感的应用
  4. 移动端应用

不适用场景

  1. 需要动态组件
  2. 复杂的条件渲染
  3. 高度动态的结构

建议:等 Vue 3.6 正式发布后,根据实际需求选择性启用。