Vue3中defineModel的set咋用?双向绑定逻辑得这么处理
defineModel是Vue3的啥新特性?
Vue3.4版本后新增的defineModel,是给组件双向绑定做的语法糖,以前写子组件支持v-model,得手动写defineProps接收modelValue,再用defineEmits触发update:modelValue事件,现在用defineModel,一行代码能同时处理“接收父组件值”和“通知父组件更新”,返回的数组里第一个是当前值,第二个就是set函数。
举个最简例子:
父组件用<ChildComponent v-model="parentVal" />,子组件里:
<script setup> const [modelValue, setModelValue] = defineModel() // modelValue 是父组件传的parentVal当前值 // setModelValue 是用来通知父组件更新parentVal的函数 </script>
defineModel里的set函数,核心作用是啥?
set函数是子组件主动修改父组件绑定值的“开关”,因为Vue的单向数据流规则,子组件不能直接改父组件传的props(虽然Vue3允许props可变,但父组件不会同步更新),所以得通过set触发一个更新事件,让父组件自己改值,再把新值传给子组件。
简单说:子组件里调用set(新值),父组件的v-model绑定值会自动更新,同时子组件里的modelValue也会同步变成新值。
咋在组件里用set修改父组件的值?举个实际场景
最常见的是自定义表单组件,比如封装一个带格式处理的输入框。
父组件用法:
<template>
<CustomInput v-model="username" />
<p>父组件的username:{{ username }}</p>
</template>
<script setup>
import { ref } from 'vue'
const username = ref('')
</script>
子组件CustomInput.vue实现:
<template>
<input
:value="modelValue"
@input="handleInput"
placeholder="请输入用户名"
/>
</template>
<script setup>
const [modelValue, setModelValue] = defineModel()
function handleInput(e) {
const inputVal = e.target.value
// 假设要做格式处理:首字母大写
const formatted = inputVal.replace(/^./, (match) => match.toUpperCase())
// 调用set,把处理后的值传给父组件
setModelValue(formatted)
}
</script>
这里关键是:输入事件里拿到用户输入,处理后用setModelValue(新值),父组件的username就会跟着变,如果直接改modelValue.value = 新值,父组件完全没反应——因为没触发更新事件,这就是set的必要性。
直接改modelValue和调用set,区别在哪?
看这段对比代码就懂了:
<script setup>
const [modelValue, setModelValue] = defineModel()
// 错误示范:直接改modelValue,父组件不更新
function wrongUpdate() {
modelValue.value = '直接修改'
}
// 正确示范:调用set,父组件会更新
function rightUpdate() {
setModelValue('通过set修改')
}
</script>
原理是:modelValue只是“当前值的引用”,和父组件的绑定值是单向同步(父组件改了子组件会变,但子组件直接改不会通知父组件),而set内部做了两件事:
- 触发
update:modelValue事件,把新值传给父组件; - 让子组件内部的
modelValue同步变成新值(因为父组件更新后会重新传值给子组件)。
多个v-model绑定,set咋处理?
如果父组件用了多个v-model(比如<Child v-model:foo="a" v-model:bar="b" />),子组件里要给defineModel传数组指定名称:
子组件代码:
<script setup>
const [foo, setFoo] = defineModel('foo')
const [bar, setBar] = defineModel('bar')
function updateFoo() {
setFoo('新的foo值') // 触发父组件a的更新
}
function updateBar() {
setBar('新的bar值') // 触发父组件b的更新
}
</script>
每个defineModel(名称)会返回对应名称的“值+set函数”,调用对应的set就只更新对应父组件的绑定值,逻辑很清晰。
和传统v-model实现比,set优势在哪?
传统写法(Vue3.4前)得手动写props和emit,代码冗余:
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
function handleChange(newVal) {
emit('update:modelValue', newVal) // 手动触发事件
}
</script>
用defineModel后,props、emit、set函数全被自动处理,代码量少一半,还避免了“忘记写emit导致更新失败”的 Bug,而且set把“触发更新”的逻辑封装成函数,可读性更强——看setModelValue(xxx)就知道这是要改父组件的值,不用再翻找emit的逻辑。
用set时容易踩的3个坑,咋避?
-
忘记调用set,直接改modelValue
新手常犯:以为modelValue是响应式的,改了就生效,子组件改父组件值必须走set,直接改只有自己能看到,父组件完全没变化。 -
set的参数类型和父组件绑定值不匹配
比如父组件绑的是数字v-model="age"(age是number),子组件set的时候传了字符串setModelValue('18'),父组件的age会变成字符串,可能导致后续逻辑报错,要保证set的参数类型和父组件绑定值一致。 -
异步操作中调用set,时机不对
比如在setTimeout里调用set,要确保组件还没被卸载,可以用onBeforeUnmount清理定时器,或者用watch监听异步结果再调用set。
结合computed或watch用set,咋玩?
场景1:给modelValue加“读写逻辑”
比如父组件传的是原始值,子组件要做格式化,同时修改时要还原格式,用computed封装:
<script setup>
const [modelValue, setModelValue] = defineModel()
// 假设父组件传的是“2024-10-01”,子组件显示“2024/10/01”,修改后再转成“-”格式
const formattedDate = computed({
get() {
return modelValue.replace(/-/g, '/')
},
set(newVal) {
const rawVal = newVal.replace(/\//g, '-')
setModelValue(rawVal) // 转成父组件需要的格式,再调用set
}
})
</script>
<template>
<input v-model="formattedDate" />
</template>
场景2:watch监听modelValue变化,再调用set做联动
比如子组件里有两个关联的v-model,改一个要同步改另一个:
<script setup>
const [foo, setFoo] = defineModel('foo')
const [bar, setBar] = defineModel('bar')
watch(foo, (newFoo) => {
// 假设foo和bar有数学关系:bar = foo * 2
setBar(newFoo * 2)
})
watch(bar, (newBar) => {
setFoo(newBar / 2)
})
</script>
这里要注意避免循环更新:比如foo改了触发bar改,bar改了又触发foo改,会无限循环,实际项目里要加条件判断,只在必要时更新。
set在组合式函数里咋复用逻辑?
很多组件有相似的“双向绑定+逻辑处理”需求,输入验证+格式处理”,可以把set封装到组合式函数里:
// useFormInput.js
export function useFormInput(setFn) {
const handleInput = (e) => {
const val = e.target.value
// 通用逻辑:去空格、验证长度等
const trimmed = val.trim()
if (trimmed.length > 10) return // 长度限制
setFn(trimmed) // 调用组件里的set函数
}
return { handleInput }
}
子组件里用:
<script setup>
const [modelValue, setModelValue] = defineModel()
const { handleInput } = useFormInput(setModelValue) // 把set传给组合式函数
</script>
<template>
<input @input="handleInput" :value="modelValue" />
</template>
这样逻辑和组件解耦,多个组件要做输入验证时,直接复用useFormInput,传各自的set函数就行。
掌握set,让双向绑定更丝滑
defineModel的set函数,本质是Vue给子组件“安全修改父组件值”的官方解决方案——既遵守单向数据流,又简化了代码,记住这几点:
- 改父组件值必须调用set,不能直接改modelValue;
- 多个v-model对应多个set,名字要和defineModel的参数一致;
- 结合computed、watch、组合式函数时,注意数据流向和类型一致;
- 避开“忘调用set、类型不匹配、异步更新”这几个坑。
实际项目里,不管是封装表单组件、联动组件,还是做复杂的双向绑定逻辑,set都能让代码更简洁易维护,多写几个自定义组件练手,自然就熟了~
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网


