Skip to content

Vue 3.5 新功能学习笔记

这份笔记根据 Vue 官方团队成员的分享,整理了 Vue 3.5 版本带来的主要新功能和优化,旨在帮助开发者快速理解和应用这些新特性。

1. defineProps 支持解构赋值与默认值

这是对开发者日常使用影响较大的一个便利性更新,简化了 props 的默认值定义。

新语法

现在可以直接在 defineProps 的解构中为属性设置默认值,代码更直观、简洁。

typescript
// 新的写法
const { count, message = 'hello' } = defineProps<{
  count: number,
  message?: string
}>()

🆚 旧语法对比

在此之前,我们需要借助 withDefaults API 来实现,写法相对繁琐。

typescript
// 旧的写法
const props = withDefaults(defineProps<{
  count: number,
  message?: string
}>(), {
  message: 'hello'
})

⚠️ 重要注意事项

当使用 watch 监听解构出来的 props 变量时,不能直接监听该变量,因为它失去了响应性。必须将其包装在一个 getter 函数中。

typescript
// 错误用法,会报错或无法正确监听
watch(count, (newVal) => {
  console.log('count changed:', newVal)
})

// 正确用法,需要提供一个 getter 函数
watch(() => count, (newVal) => {
  console.log('count changed:', newVal)
})

2. 新增 Composition API: useId

useId 用于生成一个在客户端和服务端都唯一的 ID,以解决 SSR (服务端渲染) 中的 id 不匹配问题。

核心功能

  • useId() 返回一个全局唯一的、自增的字符串ID。

⚠️ 重要注意事项:多应用实例场景

  • useId 仅能确保在单个 Vue 应用实例 (createApp) 中是唯一的
  • 如果你在同一个页面上创建了多个独立的 Vue 应用,不同应用生成的 ID 可能会重复。

🔧 解决方案

  • 为每个应用实例配置一个唯一的 idPrefix,确保最终生成的 ID 不会冲突。
javascript
// app1.js
const app1 = createApp(App)
app1.config.idPrefix = 'app1-'
app1.mount('#app1')

// app2.js
const app2 = createApp(App)
app2.config.idPrefix = 'app2-'
app2.mount('#app2')

3. 新增 Composition API: useTemplateRef

这是一个专门用于获取模板中 DOM 元素引用的 API,让意图更加清晰。

核心功能

  • useTemplateRef 使得获取 DOM 引用的代码与声明响应式状态的 ref 在语义上分离开来,避免混淆。
  • 变量名不再需要和模板中的 ref 字符串保持一致,更加灵活。

🆚 语法对比

html
<template>
  <button ref="button-ref">Click Me</button>
</template>

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

// 新的写法:专门、清晰、变量名解耦
// 可以传入泛型来精确定义元素类型
const myButtonElement = useTemplateRef<HTMLButtonElement>('button-ref')

// 旧的写法:变量名必须与 ref 值一致
const buttonRef = ref<HTMLButtonElement | null>(null) // <button ref="buttonRef">

onMounted(() => {
  console.log(myButtonElement.value) // 拿到 button DOM 元素
})
</script>

4. watch 副作用清理: onWatchCleanup

watchwatchEffect 现在提供了一个更明确的方式来清理上一次执行时产生的副作用(如事件监听、定时器等)。

核心功能

  • watch 的回调函数中,可以调用新增的 onWatchCleanup 函数,或者使用回调函数的第三个参数(也叫 onCleanup)。
  • 这个清理函数会在下一次 watch 回调执行前或者watch 被停止时自动执行。

🔧 使用场景:避免重复注册事件

javascript
import { ref, watch, onUnmounted } from 'vue'

const count = ref(1)

const handleClick = (event) => {
  console.log('clicked!', count.value, event)
}

watch(count, (newValue, oldValue, onCleanup) => {
  console.log(`count 变为 ${newValue},注册新事件`)
  window.addEventListener('click', handleClick)

  // 注册一个清理函数,它会在 count 再次变化前执行
  onCleanup(() => {
    console.log(`清理上一次 (count=${newValue}) 的事件`)
    window.removeEventListener('click', handleClick)
  })
}, { immediate: true })

// 这个功能等价于使用独立的 onWatchCleanup API
// import { onWatchCleanup } from 'vue'
// onWatchCleanup(() => { ... })

5. watchdeep 选项支持数字类型

这是一个针对性能优化的“隐藏”功能,允许你控制深度监听的最大层数。

核心功能

  • watchdeep 选项除了 true/false,现在可以接受一个数字
  • 这个数字代表了需要递归侦听的最大属性层数

🚀 解决的问题

当监听一个层级很深、数据量庞大的对象或数组时,deep: true 会递归遍历所有嵌套属性,导致昂贵的性能开销。如果明确知道只有前几层数据会变化,就可以使用 deep: <number> 来避免不必要的深度遍历。

🔧 代码示例

假设我们有一个深度嵌套的数组,但我们只关心数组本身(第一层)和数组内对象的直接属性(第二层)的变化。

javascript
const list = ref([
  {
    id: 1,
    data: {
      deep: {
        value: 100 // 第四层,不希望被监听
      }
    },
    d: 1 // 第二层,希望被监听
  }
])

watch(list, () => {
  console.log('Watcher triggered!')
}, {
  // 只监听 2 层深度
  // 第1层: list.push(), list.pop(), list[0] = ...
  // 第2层: list.value[0].d = ...
  deep: 2
})

// 这个修改会触发 watch
list.value[0].d++ 

// 这个修改不会触发 watch,因为深度超过了 2
list.value[0].data.deep.value++

6. 其他重要更新

  • 响应式系统优化: 对内部的响应式系统进行了重写,显著优化了内存占用和依赖追踪的性能。对于开发者来说,日常使用方式不变,但在处理大规模响应式对象时会有性能提升。
  • watchonce 选项 (Vue 3.4 已加入): 作为一个回顾,watch 支持 { once: true } 选项,使得监听器在触发一次后自动停止。