WSの小屋

Vue 3.4 新特性:defineModel 与响应式增强

Vue 3.4(代号 Slam Dunk)是一个值得关注的版本,带来了 defineModeldefineProps 解构默认值、toValue、以及一系列渲染性能优化。本文从实际项目角度逐个拆解。

defineModel:双向绑定的终极方案

痛点回顾

在 Vue 3.4 之前,组件双向绑定需要同时声明 props 和 emit:

<!-- 旧写法:啰嗦且容易遗漏 -->
<script setup lang="ts">
const props = defineProps<{ modelValue: string }>()
const emit = defineEmits<{ 'update:modelValue': [value: string] }>()

const updateValue = (e: Event) => {
  emit('update:modelValue', (e.target as HTMLInputElement).value)
}
</script>

<template>
  <input :value="props.modelValue" @input="updateValue" />
</template>

defineModel 基本用法

<!-- 子组件 MyInput.vue -->
<script setup lang="ts">
const model = defineModel<string>()

// model 是一个 ref,可以直接修改
function clear() {
  model.value = ''
}
</script>

<template>
  <input v-model="model" />
</template>

父组件使用方式不变:

<!-- 父组件 -->
<template>
  <MyInput v-model="searchText" />
  <MyInput v-model:query="searchText" />
</template>

多个 v-model

<script setup lang="ts">
const firstName = defineModel<string>('firstName')
const lastName = defineModel<string>('lastName')
</script>

<template>
  <input v-model="firstName" placeholder="名" />
  <input v-model="lastName" placeholder="姓" />
</template>
<!-- 父组件 -->
<UserForm v-model:firstName="fname" v-model:lastName="lname" />

带选项的 defineModel

defineModel 的第二个参数可以配置行为:

// 可选 model(不要求父组件必须传入 v-model)
const visible = defineModel<boolean>('visible', {
  default: false,
  local: true  // 本地可变,不会自动同步到父组件
})

// local: true 适用于「子组件修改后先在本地处理,再通知父组件」的场景

实战:带校验的输入组件

<script setup lang="ts">
const email = defineModel<string>({
  set(value) {
    // 写入前校验,无效值不更新
    if (value && !value.includes('@')) return email.value
    return value
  },
  get(value) {
    // 读取时转换
    return value?.trim() ?? ''
  }
})

const isValid = computed(() => /^[^@]+@[^@]+\.[^@]+$/.test(email.value))
</script>

<template>
  <input v-model="email" :class="{ invalid: !isValid }" />
</template>

defineProps 解构默认值

Vue 3.4 之前,定义 props 默认值必须使用 withDefaults

// 旧写法
const props = withDefaults(defineProps<{
  title?: string
  count?: number
}>(), {
  title: '默认标题',
  count: 0
})

Vue 3.4 支持直接在解构中设置默认值,编译器会自动处理响应性:

// 新写法:更简洁
const { title = '默认标题', count = 0 } = defineProps<{
  title?: string
  count?: number
}>()

// title 和 count 依然是响应式的!
// 编译器会自动生成 withDefaults 等价代码

注意:Vue 3.4 的响应式 props 解构需要配合 Volar 3.x(Vue 官方 VS Code 插件)使用。

toValue:统一 MaybeRef 的访问

Vue 3.4 新增 toValue(),统一访问 refgetterplain value 三种输入:

import { toValue, type MaybeRefOrGetter } from 'vue'

// before: 需要分别处理
function normalize(input: MaybeRefOrGetter<string>) {
  if (typeof input === 'function') return input()
  if (isRef(input)) return input.value
  return input
}

// after: 一行搞定
function normalize(input: MaybeRefOrGetter<string>) {
  return toValue(input)
}

实戾示例:一个接受多种输入的 composable:

function useFetch<T>(url: MaybeRefOrGetter<string>) {
  const data = ref<T>()
  const error = ref<Error>()
  const loading = ref(false)

  watchEffect(async () => {
    // url 可能是 string / ref / getter,toValue 统一处理
    const resolvedUrl = toValue(url)
    if (!resolvedUrl) return

    loading.value = true
    try {
      data.value = await fetch(resolvedUrl).then(r => r.json())
    } catch (e) {
      error.value = e as Error
    } finally {
      loading.value = false
    }
  })

  return { data, error, loading }
}

// 调用方可以灵活传参
useFetch('/api/users')                    // 普通字符串
useFetch(computed(() => `/api/users/${id}`)) // 计算属性
useFetch(() => dynamicUrl.value)           // getter

渲染性能优化

Vue 3.4 在渲染层做了大量优化,无需改代码即可受益:

1. 静态提升改进

编译器更激进地提升静态节点,减少不必要的 patch:

<template>
  <!-- 这部分在 3.4 中会被完全提升,不会参与 diff -->
  <div class="header">
    <h1>标题</h1>
    <p>固定描述文字</p>
  </div>

  <!-- 只有这里会参与响应式更新 -->
  <div>{{ dynamicContent }}</div>
</template>

2. v-once 和 v-memo 优化

v-oncev-memo 在 3.4 中更高效,适用于大列表局部更新:

<template>
  <div v-for="item in list" :key="item.id" v-memo="[item.selected]">
    <!-- 只有 item.selected 变化时才会重新渲染这个节点 -->
    <span :class="{ active: item.selected }">{{ item.name }}</span>
    <span>{{ item.detail }}</span>
  </div>
</template>

defineEmits 更严格的类型校验

const emit = defineEmits<{
  // 3.4 支持更精确的 payload 类型校验
  change: [value: string, oldValue: string]
  submit: [payload: { id: number; data: FormData }]
  // 无 payload 的事件
  reset: []
}>()

// 调用时编译器会校验参数类型
emit('change', 'new', 'old')  // ✅
emit('change', 'new')         // ❌ 缺少参数
emit('submit', { id: 1 })     // ❌ 缺少 data

useTemplateRef:更安全的模板引用

Vue 3.5 引入了 useTemplateRef,Vue 3.4 可以先用 ref 方式:

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

const inputRef = ref<HTMLInputElement>()

onMounted(() => {
  inputRef.value?.focus()
})
</script>

<template>
  <input ref="inputRef" />
</template>

升级建议

# 检查当前版本
npx vue --version

# 升级到 3.4+
pm install vue@^3.4

# 如果用了 @vitejs/plugin-vue,同步升级
pnpm add -D @vitejs/plugin-vue@latest

# Nuxt 项目升级
npx nuxi upgrade

升级注意事项

  1. defineModel 需要显式启用(3.4 默认稳定可用,但旧版本需在 vite.config.ts 中配置)
  2. Volar 版本:确保 VS Code 的 Vue 插件升级到 3.x
  3. 响应式 props 解构:旧的 withDefaults 写法仍然兼容,可以渐进迁移

总结

特性 影响 优先级
defineModel 大幅简化组件双向绑定代码 ⭐⭐⭐
props 解构默认值 更简洁的 props 定义 ⭐⭐
toValue 统一 MaybeRef 处理 ⭐⭐
渲染优化 无需改代码即可提升性能 ⭐⭐⭐
emit 严格类型 更安全的类型校验

Vue 3.4 是一个增量升级的版本——不需要大改代码,但每改一处都能让代码更简洁。建议先从 defineModel 开始迁移,收益最直接。

Comments | 0条评论