vue新人对v-model的思考

159 阅读3分钟

案例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组件 效果如下 image.png
它的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'一点
就发现很混乱了 继而引发了这篇文章

总结

  1. v-model好用 但只适用于简单场景
  2. 当v-model和watch同时出现时必须小心
  3. 多看几遍你可能不需要effect,搞清楚一段代码应该属于事件函数还是watch