Vue 表单避坑(一):为什么 v-model 绑定对象属性会偷偷修改父组件数据?

场景引入

在 Vue 项目里,表单组件几乎无处不在。为了提高复用性,我们常常会把一堆输入框封装成一个"大表单组件",然后通过 v-model 直接绑定一个对象给外部组件:

html 复制代码
<!-- App.vue -->
<script setup>
  import { ref } from 'vue'
  import MyForm from './MyForm.vue'

  const data = ref({  })
</script>

<template>
  <MyForm v-model="data" />
</template>

MyForm.vue 里,我们定义一个 model,接着直接把 model 的属性绑定到 MyInput 上:

html 复制代码
<!-- MyForm.vue -->
<script setup>
  import MyInput from './MyInput.vue'
  import { computed } from 'vue'
  const props = defineProps({
    modelValue: Object
  });
  const emit = defineEmits(['update:modelValue']);
  const model = computed({ 
    get: () => props.modelValue, 
    set: (v) => emit('update:modelValue', v)  
  })
</script>

<template>
  <div>开始:<MyInput v-model="model.start" /></div> 
  <div>结束:<MyInput v-model="model.end" /></div>
</template>

最后是简单的 MyInput.vue

html 复制代码
<!-- MyInput.vue -->
<script setup>
  import { computed } from 'vue'
  const props = defineProps({
    modelValue: Number
  });
  const emit = defineEmits(['update:modelValue']);
  const value = computed({ 
    get: () => props.modelValue, 
    set: (v) => emit('update:modelValue', v)  
  })
</script>

<template>
  <span>
    <span>{{ value }}</span>
    <button @click="value = Date.now()">更新</button>
  </span>
</template>

看起来一气呵成,干净又优雅,不是吗?

然而,这段代码已经违背了单向数据流原则。

先做个实验:把 v-model 换成 :model-value

App.vue 里的 v-model 改成 :model-value(也就是只传 prop,不监听 update 事件):

html 复制代码
<!-- App.vue -->
<script setup>
  import { ref } from 'vue'
  import MyForm from './MyForm.vue'

  const data = ref({  })
</script>

<template>
  <MyForm :model-value="data" />
</template>

按常理,此时 data 不应该被子组件修改,因为父组件没有监听 update 事件。

但是点击按钮后你会发现------data 还是被改了! (不信可以去 Vue Playground 试试)

这就怪了,明明没有监听 update 事件,数据怎么变的?因为子组件直接修改了同一个对象的属性,绕过了事件机制。

问题的本质:v-model 直接绑定属性值时发生了什么?

MyForm.vue 中,我们写了 <MyInput v-model="model.start" />v-model="model.start" 在 Vue 3 中会被展开为:

html 复制代码
<MyInput
  :model-value="model.start"
  @update:model-value="v => model.start = v"
/>

model.start 是什么?是 modelValue 的一个属性,直接指向父组件的 data。所以 v => model.start = v 这一赋值直接修改了父组件的对象属性 ,根本没有触发 MyForm.vueupdate:model-value 事件。

换句话说,MyForm.vue 没有发出 update:model-value 事件,App.vue 完全不知道自己数据已经被改了。


你还可以把 MyForm.vue 中的 model 调整为

js 复制代码
const model = computed({ 
    get: () => props.modelValue, 
    set: (v) => {
      console.log('MyForm.vue update:modelValue', v)
      emit('update:modelValue', v) 
    } 
})

在控制台里,没有输出内容。console.log('MyForm.vue update:modelValue', v) 完全不会执行到

单向数据流到底是什么?

Vue 的单向数据流规定:

  • 父组件通过 props 把数据交给子组件。
  • 子组件不能直接修改 props,必须通过 emit 事件 通知父组件,由父组件自己修改数据。
  • 数据永远是从父 → 子,事件是从子 → 父。

v-model 本身是符合单向数据流的------前提是你通过事件更新的是整个数据,而不是直接修改对象的属性。

在上面的例子中,虽然我们用了 v-model,但实际更新时是直接改了对象的属性,跳过了通知 App.vue 更新数据的步骤,在 MyForm.vue 中偷偷改了数据,违背了设计原则。

修复方案

既然直接绑定属性会导致"暗箱操作",那我们就改成显式的方式------**每次字段更新都通过一个 update 函数,生成一个新对象来赋值。

html 复制代码
<!-- MyForm.vue -->
<script setup>
  import MyInput from './MyInput.vue'
  import { computed } from 'vue'
  const props = defineProps({
    modelValue: Object
  });
  const emit = defineEmits(['update:modelValue']);
  const model = computed({ 
    get: () => props.modelValue, 
    set: (v) => {
      console.log('MyForm.vue update:modelValue', v)
      emit('update:modelValue', v) 
    } 
  })
  function update(k, v) {
    model.value = {
      ...model.value,
      [k]: v
    }
  }
</script>

<template>
  <div>开始:<MyInput 
    :model-value="model.start" 
    @update:model-value="v => update('start', v)" 
  /></div> 
  <div>结束:<MyInput 
    :model-value="model.end" 
    @update:model-value="v => update('end', v)" 
  /></div>
</template>

此时,console.log('MyForm.vue update:modelValue', v) 代码正常执行。

App.vue<MyForm :model-value="data" /> 时,内层无法更新外层数据。

小结

在组件化设计中,数据的"所有权"必须与"修改权"严格对应。 App.vue 作为数据的拥有者,应该掌握唯一的修改权限;MyForm.vue只能通过"申请-批准"的机制(即 emit 事件)来请求变更。这是保证状态可预测、可调试的基石。

相关推荐
小码哥_常2 小时前
Android与JS交互:解锁混合开发的魔法之门
前端
leafyyuki2 小时前
如何优雅地上传大文件?分片上传实战指南
前端·音视频开发
Mintopia2 小时前
现代 Vue 3 页面组件文件安排与通信实践
前端
只会cv的前端攻城狮2 小时前
兼容性地狱-Uniapp钉钉小程序环境隔离踩坑实录
前端·uni-app
赵_叶紫2 小时前
Node.js 知识点梳理与实战代码
前端
IT_陈寒3 小时前
JavaScript这5个隐藏技巧,90%的开发者都不知道!
前端·人工智能·后端
明月_清风4 小时前
小程序云函数:从入门到全栈的“降维打击”指南
前端·微信小程序·小程序·云开发
wuhen_n4 小时前
告别 Options API:为什么 Composition API 是逻辑复用的未来?
前端·javascript·vue.js
明月_清风4 小时前
前端异常捕获:从“页面崩了”到“精准定位”的实战架构
前端·javascript·监控