案例1
首先看一个简单的需求 封装一个弹窗组件
以前虽然工作一直用的react 但是之前也学习过vue 对vue的写法和语法还有点印象
所以我写下了这样的东西
const props = defineProps(["modelValue"]);
<el-dialog v-model="props.modelValue"></el-dialog>
当然是报错了
类型提示上看props.visible是一个readonly属性
eslint也提示v-model必须是一个ref属性
回头看看文档 v-model只是一个语法糖
它的本质是 :modelValue+@update:modelValue
所以要写成这个样子
const props = defineProps(["modelValue"]);
const emits = defineEmits(["update:modelValue"]);
<el-dialog
:modelValue="props.modelValue"
@update:modelValue="(val) => emits('update:modelValue', val)"
></el-dialog>
如何使用v-model
我认为v-model本质上是对“状态”这一概念的革新
就像“组件”把html和js组织起来
ref和v-model也把状态的值和更新方式组织起来了
理论上 用户只需要创建ref然后放进v-model里面就可以了
不必操心什么setState 也不担心闭包问题 只需要专注于组件本身
其余的事情有computed和watch处理
看起来相对于react vue似乎更接近UI=fn(state)
不过它其实并没有想象中的好
它只是个简单的语法糖 帮助开发者少写点代码罢了
即使是简单的封装弹窗 也没办法一直v-model下去 必须手写更新函数
当然 强行v-model也不是不可以 就是代码会变成下面这样子
const props = defineProps(["modelValue"]);
const emits = defineEmits(["update:modelValue"]);
const thisVisible = ref(props.modelValue)
watch(()=>props.modelValue,newVal=>{
thisVisible.value=newVal
})
watch(()=>thisVisible.value,newVal=>{
emits('update:modelValue',newVal)
})
<el-dialog v-model="thisVisible"></el-dialog>
↑这段代码来自公司的项目 当时看完我直接神志不清了
v-model和watch让项目围绕响应式变量展开 不过不应该滥用它们
复杂组件
v-model的优势区间是简单的组件
无论是受控还是非受控 用v-model可以省去大量重复的代码
但当组件复杂起来 开发者必须手动控制更新函数时 v-model就会成为掣肘
v-model只是个语法糖 开发者可以自由选择是否使用 但架不住组件库(elementui)也在用这个
一串modelValue和update:modelValue 真是令人头昏眼花
拿我最近做的一个需求举例 要做一个DatePicker组件 效果如下
它的v-model是一个字符串 作用是选择时间谓词和日期
图中就是等效于字符串'<2025-03-05'
此外还有>和= '2025-03-05'与'=2025-03-05'没有区别
代码如下
const props = defineProps(["modelValue"]);
const emits = defineEmits(["update:modelValue"]);
const dateSignalList = [
{ label: "小于", value: "<" },
{ label: "等于", value: "=" },
{ label: "大于", value: ">" },
];
const currentSignal = computed(() => {
for (let i = 0; i < dateSignalList.length; ++i) {
if (props.modelValue?.startsWith(dateSignalList[i].value)) {
return dateSignalList[i].value;
}
}
if (dayjs(props.modelValue).isValid()) {
return "=";
}
return null;
});
const formatString = "YYYY-MM-DD";
const currentDate = computed(() => {
const date = dayjs(props.modelValue.replace(currentSignal.value, ""));
if (date.isValid()) return date.format(formatString);
return null;
});
const onSelectSignal = (signal) => {
emits("update:modelValue", signal + currentDate.value);
};
const onSelectDate = (date) => {
emits("update:modelValue", currentSignal.value + date);
};
<template>
<el-select :modelValue="currentSignal" @update:modelValue="onSelectSignal">
<el-option
v-for="{ label, value } in dateSignalList"
:key="value"
:label="label"
:value="value"
></el-option>
</el-select>
<el-date-picker
:modelValue="currentDate"
@update:modelValue="onSelectDate"
:value-format="formatString"
></el-date-picker>
</template>
一开始写的很顺畅 因为把握到需求的核心:
currentSignal和currentDate是完全受控不可变的
后来我尝试学习vue以状态为核心的风格
尝试使用v-model和watch把这个组件变得更'vue'一点
就发现很混乱了 继而引发了这篇文章
总结
- v-model好用 但只适用于简单场景
- 当v-model和watch同时出现时必须小心
- 多看几遍你可能不需要effect,搞清楚一段代码应该属于事件函数还是watch