广州天地航模 發表於 2026-3-7 12:56:00

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

<h1 data-id="heading-0">🧑‍💻 写在开头</h1>
<p>点赞 + 收藏 === 学会🤣🤣🤣</p>
<h2 data-id="heading-0">场景引入</h2>
<p>在 Vue 项目里,表单组件几乎无处不在。为了提高复用性,我们常常会把一堆输入框封装成一个“大表单组件”,然后通过&nbsp;<code>v-model</code>&nbsp;直接绑定一个对象给外部组件:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">&lt;!-- App.vue --&gt;
&lt;script setup&gt;
import { ref } from 'vue'
import MyForm from './MyForm.vue'

const data = ref({})
&lt;/script&gt;

&lt;template&gt;
&lt;MyForm v-model="data" /&gt;
&lt;/template&gt;</pre>
</div>
<p>在&nbsp;<code>MyForm.vue</code>&nbsp;里,我们定义一个&nbsp;<code>model</code>,接着直接把&nbsp;<code>model</code>&nbsp;的属性绑定到&nbsp;<code>MyInput</code>&nbsp;上:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">&lt;!-- MyForm.vue --&gt;
&lt;script setup&gt;
import MyInput from './MyInput.vue'
import { computed } from 'vue'
const props = defineProps({
    modelValue: Object
});
const emit = defineEmits(['update:modelValue']);
const model = computed({
    get: () =&gt; props.modelValue,
    set: (v) =&gt; emit('update:modelValue', v)
})
&lt;/script&gt;

&lt;template&gt;
&lt;div&gt;开始:&lt;MyInput v-model="model.start" /&gt;&lt;/div&gt;
&lt;div&gt;结束:&lt;MyInput v-model="model.end" /&gt;&lt;/div&gt;
&lt;/template&gt;</pre>
</div>
<p>最后是简单的&nbsp;<code>MyInput.vue</code>:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">&lt;!-- MyInput.vue --&gt;
&lt;script setup&gt;
import { computed } from 'vue'
const props = defineProps({
    modelValue: Number
});
const emit = defineEmits(['update:modelValue']);
const value = computed({
    get: () =&gt; props.modelValue,
    set: (v) =&gt; emit('update:modelValue', v)
})
&lt;/script&gt;

&lt;template&gt;
&lt;span&gt;
    &lt;span&gt;{{ value }}&lt;/span&gt;
    &lt;button @click="value = Date.now()"&gt;更新&lt;/button&gt;
&lt;/span&gt;
&lt;/template&gt;</pre>
</div>
<div>
<div>
<p>看起来一气呵成,干净又优雅,不是吗?</p>
<p><strong>然而,这段代码已经违背了单向数据流原则。</strong></p>
<h2 data-id="heading-1">先做个实验:把 v-model 换成 :model-value</h2>
<p>把&nbsp;<code>App.vue</code>&nbsp;里的&nbsp;<code>v-model</code>&nbsp;改成&nbsp;<code>:model-value</code>(也就是只传 prop,不监听&nbsp;<code>update</code>&nbsp;事件):</p>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">&lt;!-- App.vue --&gt;
&lt;script setup&gt;
import { ref } from 'vue'
import MyForm from './MyForm.vue'

const data = ref({})
&lt;/script&gt;

&lt;template&gt;
&lt;MyForm :model-value="data" /&gt;
&lt;/template&gt;</pre>
</div>
<div>
<div>
<p>按常理,此时&nbsp;<code>data</code>&nbsp;不应该被子组件修改,因为父组件没有监听&nbsp;<code>update</code>&nbsp;事件。</p>
<p>但是点击按钮后你会发现——<strong><code>data</code>&nbsp;还是被改了!</strong> (不信可以去&nbsp;Vue Playground&nbsp;试试)</p>
<p>这就怪了,明明没有监听&nbsp;<code>update</code>&nbsp;事件,数据怎么变的?<strong>因为子组件直接修改了同一个对象的属性,绕过了事件机制。</strong></p>
<h2 data-id="heading-2">问题的本质:v-model 直接绑定属性值时发生了什么?</h2>
<p>在&nbsp;<code>MyForm.vue</code>&nbsp;中,我们写了&nbsp;<code>&lt;MyInput v-model="model.start" /&gt;</code>。<code>v-model="model.start"</code>&nbsp;在 Vue 3 中会被展开为:</p>
</div>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">&lt;MyInput
:model-value="model.start"
@update:model-value="v =&gt; model.start = v"
/&gt;</pre>
</div>
<div>
<div>
<p><code>model.start</code>&nbsp;是什么?是 <code>modelValue</code> 的一个属性,直接指向父组件的 <code>data</code>。所以 <code>v =&gt; model.start = v</code> 这一赋值<strong>直接修改了父组件的对象属性</strong>,根本没有触发&nbsp;<code>MyForm.vue</code>&nbsp;的&nbsp;<code>update:model-value</code>&nbsp;事件。</p>
<p>换句话说,<code>MyForm.vue</code>&nbsp;没有发出&nbsp;<code>update:model-value</code> 事件,<code>App.vue</code> 完全不知道自己数据已经被改了。</p>
<hr>
<p>你还可以把 <code>MyForm.vue</code> 中的 <code>model</code> 调整为</p>
</div>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">const model = computed({
    get: () =&gt; props.modelValue,
    set: (v) =&gt; {
      console.log('MyForm.vue update:modelValue', v)
      emit('update:modelValue', v)
    }
})</pre>
</div>
<div>
<div>
<p>在控制台里,没有输出内容。<code>console.log('MyForm.vue update:modelValue', v)</code> 完全不会执行到</p>
<h2 data-id="heading-3">单向数据流到底是什么?</h2>
<p>Vue 的单向数据流规定:</p>
<ul>
<li>父组件通过&nbsp;<strong>props</strong>&nbsp;把数据交给子组件。</li>
<li>子组件不能直接修改 props,必须通过&nbsp;<strong>emit 事件</strong>&nbsp;通知父组件,由父组件自己修改数据。</li>
<li>数据永远是从父 → 子,事件是从子 → 父。</li>
</ul>
<p><code>v-model</code>&nbsp;本身是符合单向数据流的——前提是你<strong>通过事件更新的是整个数据</strong>,而不是直接修改对象的属性。</p>
<p>在上面的例子中,虽然我们用了&nbsp;<code>v-model</code>,但实际更新时是直接改了对象的属性,跳过了通知&nbsp;<code>App.vue</code>&nbsp;更新数据的步骤,在 <code>MyForm.vue</code> 中偷偷改了数据,违背了设计原则。</p>
<h2 data-id="heading-4">修复方案</h2>
<p>既然直接绑定属性会导致“暗箱操作”,那我们就改成显式的方式——**每次字段更新都通过一个&nbsp;<code>update</code>&nbsp;函数,生成一个新对象来赋值。</p>
<div class="code-block-extension-header">
<div class="code-block-extension-headerLeft">
<div class="code-block-extension-foldBtn">
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">&lt;!-- MyForm.vue --&gt;
&lt;script setup&gt;
import MyInput from './MyInput.vue'
import { computed } from 'vue'
const props = defineProps({
    modelValue: Object
});
const emit = defineEmits(['update:modelValue']);
const model = computed({
    get: () =&gt; props.modelValue,
    set: (v) =&gt; {
      console.log('MyForm.vue update:modelValue', v)
      emit('update:modelValue', v)
    }
})
function update(k, v) {
    model.value = {
      ...model.value,
      : v
    }
}
&lt;/script&gt;

&lt;template&gt;
&lt;div&gt;开始:&lt;MyInput
    :model-value="model.start"
    @update:model-value="v =&gt; update('start', v)"
/&gt;&lt;/div&gt;
&lt;div&gt;结束:&lt;MyInput
    :model-value="model.end"
    @update:model-value="v =&gt; update('end', v)"
/&gt;&lt;/div&gt;
&lt;/template&gt;</pre>
</div>
<div>
<div>
<p>此时,<code>console.log('MyForm.vue update:modelValue', v)</code> 代码正常执行。</p>
<p><code>App.vue</code> 中 <code>&lt;MyForm :model-value="data" /&gt;</code> 时,内层无法更新外层数据。</p>
<h2 data-id="heading-5">小结</h2>
<p><strong>在组件化设计中,数据的“所有权”必须与“修改权”严格对应。</strong> &nbsp;<code>App.vue</code> 作为数据的拥有者,应该掌握唯一的修改权限;<code>MyForm.vue</code>只能通过“申请-批准”的机制(即&nbsp;<code>emit</code>&nbsp;事件)来请求变更。这是保证状态可预测、可调试的基石。</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div>
<h3 id="tid-D8HBxE">如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。</h3>
</div>
<p><em><img src="https://img2024.cnblogs.com/blog/2149129/202501/2149129-20250122165814748-630765389.png" alt="" loading="lazy"></em></p>
</div><br><br>
来源:https://www.cnblogs.com/smileZAZ/p/19682693
頁: [1]
查看完整版本: Vue 表单避坑:为什么 v-model 绑定对象属性会偷偷修改父组件数据?