Vue 3.4 新特性:defineModel 与响应式增强
Vue 3.4(代号 Slam Dunk)是一个值得关注的版本,带来了 defineModel、defineProps 解构默认值、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(),统一访问 ref、getter、plain 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-once 和 v-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
升级注意事项
- defineModel 需要显式启用(3.4 默认稳定可用,但旧版本需在
vite.config.ts中配置) - Volar 版本:确保 VS Code 的 Vue 插件升级到 3.x
- 响应式 props 解构:旧的
withDefaults写法仍然兼容,可以渐进迁移
总结
| 特性 | 影响 | 优先级 |
|---|---|---|
defineModel |
大幅简化组件双向绑定代码 | ⭐⭐⭐ |
| props 解构默认值 | 更简洁的 props 定义 | ⭐⭐ |
toValue |
统一 MaybeRef 处理 | ⭐⭐ |
| 渲染优化 | 无需改代码即可提升性能 | ⭐⭐⭐ |
| emit 严格类型 | 更安全的类型校验 | ⭐ |
Vue 3.4 是一个增量升级的版本——不需要大改代码,但每改一处都能让代码更简洁。建议先从 defineModel 开始迁移,收益最直接。
Comments | 0条评论