reactive 对象重新赋值后,表单为什么没有恢复默认值

本文是「Vue 页面行为拆解」系列的第 1 篇。

下面使用 Vue 3 + Composition API 的简化表单场景说明问题。

设想一个用户编辑页面。接口数据填入表单后,用户点击重置 ,页面应该恢复为空白状态。开发者重新创建了一份 reactive 对象,控制台没有报错,输入框却仍然显示原来的姓名和部门。

下面是表单组件中的关键代码。读者需要观察重置函数修改的是表单属性,还是保存表单对象的局部变量。

vue 复制代码
<script setup>
import { reactive } from 'vue'

let form = reactive({
  name: 'Alice',
  department: '前端'
})

function resetForm() {
  form = reactive({
    name: '',
    department: ''
  })
}
</script>

<template>
  <input v-model="form.name">
  <input v-model="form.department">
  <button @click="resetForm">重置</button>
</template>

页面初始显示:

text 复制代码
姓名:Alice
部门:前端

点击重置后,两个输入框可能仍然保持原值。代码确实创建了一个新的响应式对象,但这次赋值没有修改模板上一次渲染时依赖的那组属性,也没有单独产生一次可追踪的状态更新。

一、首次渲染时,模板读取的是哪一个对象

reactive 接收一个对象,并返回这个对象的响应式代理。组件模板读取代理上的属性时,Vue 会记录本次渲染使用了哪些状态,这些属性后续发生变化时,组件才有依据安排更新。

下面把初始对象单独保存下来,观察模板使用的值来自哪里。

vue 复制代码
<script setup>
import { reactive, isReactive } from 'vue'

const initialData = {
  name: 'Alice',
  department: '前端'
}

const form = reactive(initialData)

console.log(isReactive(form))
console.log(form === initialData)
</script>

<template>
  <p>{{ form.name }} / {{ form.department }}</p>
</template>

控制台输出:

text 复制代码
true
false

form 是响应式对象,并且与传入的原始对象不是同一个引用。模板渲染时读取的是响应式对象上的 namedepartment

可以把第一次渲染简化为:

text 复制代码
模板读取 form.name
→ Vue 记录当前渲染依赖这个属性
→ 模板读取 form.department
→ Vue 记录第二个属性
→ 页面显示 Alice / 前端

后续执行:

ts 复制代码
form.name = 'Bob'

写入发生在同一个响应式对象上,Vue 能够把这次变化与之前记录的读取关系对应起来,组件随后重新渲染。

当前问题中的重置函数没有修改原对象上的属性,它把局部变量指向了另一个对象。两种操作看起来都能让 form.name 变为空字符串,进入响应式更新流程的方式却不同。

二、重新赋值改变了局部变量,却没有触发原对象更新

回到最初的重置函数:

ts 复制代码
function resetForm() {
  form = reactive({
    name: '',
    department: ''
  })
}

这段代码完成了两件事:

text 复制代码
创建一个新的响应式对象
→ 让局部变量 form 指向新对象

它没有修改旧对象的 namedepartment

模板上一次渲染时读取的是旧对象的属性。局部变量重新赋值属于普通 JavaScript 变量赋值,变量本身并不是一个 ref,所以这一步不会单独通知组件重新渲染。

可以用两个对象表示这次变化:

text 复制代码
点击前:
form ──→ 响应式对象 A
         name: Alice
         department: 前端

点击后:
form ──→ 响应式对象 B
         name: ''
         department: ''

模板已记录的依赖仍来自对象 A

这次问题同时出现了旧响应式对象、新响应式对象和模板依赖,只看赋值代码不容易确认页面仍在依赖哪一层。下面把重置前后的引用关系放在同一张图中。

这也解释了一个看起来不太稳定的现象:点击重置后页面没有变化,之后修改了组件中的其他响应式状态,页面又突然显示了新表单。

其他状态变化可能安排了一次新的渲染。重新渲染时,模板再次读取局部变量,此时它已经指向对象 B,于是页面显示空值,并开始记录对象 B 上的属性。

问题并没有自行消失。重置操作仍然依赖另一项状态碰巧触发渲染,页面行为因此难以预测。

同时 Vue 官方文档也明确说明,reactive 的响应式跟踪依赖属性访问,替换整个对象会丢失与原引用建立的响应式连接。

三、给原对象的属性赋值为什么能够更新页面

表单字段数量较少时,可以直接修改原响应式对象:

vue 复制代码
<script setup>
import { reactive } from 'vue'

const form = reactive({
  name: 'Alice',
  department: '前端'
})

function resetForm() {
  form.name = ''
  form.department = ''
}
</script>

<template>
  <input v-model="form.name">
  <input v-model="form.department">
  <button @click="resetForm">重置</button>
</template>

点击按钮后,页面结果为:

text 复制代码
姓名输入框:空
部门输入框:空

这里的 form 始终指向同一个响应式对象。重置函数修改了模板读取过的两个属性,所以组件能够根据这些变化更新输入框。

执行顺序是:

text 复制代码
用户点击重置
→ 修改 form.name
→ 修改 form.department
→ Vue 安排组件更新
→ 输入框显示空值

这套写法适合字段少、默认值稳定的表单。字段逐渐增加后,手动为每个属性赋值容易漏掉新增字段,因此通常还需要统一创建默认数据。

四、Object.assign 怎样在保留对象引用的同时重置字段

可以使用一个函数生成默认表单,再把默认值写入原响应式对象。

下面这段代码仍位于表单组件中。重点观察 form 没有被替换,变化发生在它的属性上。

vue 复制代码
<script setup>
import { reactive } from 'vue'

function createDefaultForm() {
  return {
    name: '',
    department: '',
    enabled: true
  }
}

const form = reactive(createDefaultForm())

function fillExample() {
  Object.assign(form, {
    name: 'Alice',
    department: '前端',
    enabled: false
  })
}

function resetForm() {
  Object.assign(form, createDefaultForm())
}
</script>

<template>
  <input v-model="form.name">
  <button @click="fillExample">填充</button>
  <button @click="resetForm">重置</button>
</template>

点击填充按钮后,姓名变为 Alice,再点击重置按钮,姓名恢复为空字符串,enabled 恢复为 true

Object.assign 会把默认对象中的属性逐项写入现有的响应式对象。每个写入仍然经过原来的响应式代理,因此模板依赖可以继续使用。

它解决了两个问题:

  • 表单对象的引用保持不变;
  • 默认值集中在一个函数中,新增字段时有统一修改位置。

这项处理仍有边界。Object.assign 只处理来源对象中存在的第一层属性,不会自动删除表单中额外出现的字段。

例如下面的表单在运行过程中增加了 temporaryNote,默认对象中没有这个字段:

vue 复制代码
<script setup>
import { reactive } from 'vue'

const form = reactive({
  name: 'Alice',
  temporaryNote: '仅本次编辑使用'
})

function resetForm() {
  Object.assign(form, {
    name: ''
  })
}

resetForm()

console.log(form)
</script>

控制台结果中仍然包含:

text 复制代码
{
  name: '',
  temporaryNote: '仅本次编辑使用'
}

如果表单结构允许动态添加字段,重置逻辑还需要明确哪些字段应该删除,不能把 Object.assign 当作让对象与默认对象完全一致的操作。

五、嵌套对象重置时还要考虑哪些引用

假设表单中包含地址和标签:

vue 复制代码
<script setup>
import { reactive } from 'vue'

function createDefaultForm() {
  return {
    name: '',
    address: {
      city: '',
      detail: ''
    },
    tags: []
  }
}

const form = reactive(createDefaultForm())

function resetForm() {
  Object.assign(form, createDefaultForm())
}
</script>

<template>
  <input v-model="form.address.city">
  <span>标签数:{{ form.tags.length }}</span>
  <button @click="resetForm">重置</button>
</template>

点击重置后,地址和标签会恢复为默认值。这里发生的是第一层属性写入:

text 复制代码
form.address → 换成新的地址对象
form.tags    → 换成新的数组

Vue 会让这些新对象继续参与响应式处理,当前模板可以跟着更新。

Object.assign 没有保留旧的嵌套对象引用。假设其他代码提前保存了:

ts 复制代码
const currentAddress = form.address

重置后,currentAddress 仍然指向旧地址对象,form.address 已经指向新对象。继续修改旧引用,不会改变当前表单中的地址。

如果子组件始终通过 props 读取最新的 form.address,父组件更新后可以获得新对象。若子组件在初始化时又把地址复制成本地状态,则需要单独处理组件属性和本地状态的同步,这属于另一个数据流问题。

当业务要求保留嵌套对象引用时,可以分别修改嵌套属性:

vue 复制代码
<script setup>
import { reactive } from 'vue'

const form = reactive({
  address: {
    city: '广州',
    detail: '天河区'
  },
  tags: ['重点用户']
})

function resetForm() {
  Object.assign(form.address, {
    city: '',
    detail: ''
  })

  form.tags.splice(0)
}
</script>

<template>
  <p>{{ form.address.city }}</p>
  <p>{{ form.tags.length }}</p>
  <button @click="resetForm">重置</button>
</template>

点击重置后,城市为空,标签数量变为 0,同时地址对象和标签数组的原引用仍然保留。

代价是重置代码需要了解嵌套结构。表单层级较深时,逐层维护会增加修改成本,应结合组件是否真的共享这些引用再决定。

六、需要替换整个对象时,为什么 ref 更合适

有些页面把接口响应整体作为编辑草稿。切换用户或恢复快照时,代码希望直接替换整份数据,而不是逐个修改属性。

这类场景可以使用 refref 保存一个可替换的响应式值,脚本中通过 .value 读取和赋值,模板会自动读取其中的值。

下面观察根对象替换后的页面结果:

vue 复制代码
<script setup>
import { ref } from 'vue'

function createDefaultForm() {
  return {
    name: '',
    department: ''
  }
}

const form = ref({
  name: 'Alice',
  department: '前端'
})

function resetForm() {
  form.value = createDefaultForm()
}
</script>

<template>
  <input v-model="form.name">
  <input v-model="form.department">
  <button @click="resetForm">重置</button>
</template>

点击重置后,两个输入框恢复为空。

这次赋值修改的是:

ts 复制代码
form.value

.value 本身是响应式状态。替换整个对象会触发依赖更新,新对象也会继续参与响应式处理。

reactiveref 在当前场景中的区别可以整理为:

方案 更适合的更新方式 当前场景中的代价
reactive 保持根对象不变,修改其中属性 整体重置需要写入原对象
ref 允许替换整个根值 脚本中访问和赋值需要 .value
嵌套属性逐项重置 保留嵌套对象与数组引用 重置代码需要了解具体结构

选择依据应该来自数据更新方式。如果表单从创建到销毁都围绕同一个对象修改,reactive 比较自然;如果业务频繁替换整份草稿,ref 能让更新动作更直接。

七、nextTick 为什么不能补上这次响应式更新

看到页面没有变化后,有时会在重置函数中加入 nextTick

vue 复制代码
<script setup>
import {
  reactive,
  nextTick
} from 'vue'

let form = reactive({
  name: 'Alice'
})

async function resetForm() {
  form = reactive({
    name: ''
  })

  await nextTick()
  console.log('DOM 更新等待结束')
}
</script>

<template>
  <input v-model="form.name">
  <button @click="resetForm">重置</button>
</template>

点击按钮后,控制台会打印:

text 复制代码
DOM 更新等待结束

但输入框仍可能显示 Alice

nextTick 用于等待当前已经安排的 DOM 更新完成。这里重新给普通局部变量赋值,没有因此安排组件更新,所以等待一次更新周期不会自动建立新的响应式关系。

它适合下面这种流程:

text 复制代码
修改响应式状态
→ Vue 安排 DOM 更新
→ nextTick 等待该次更新完成
→ 读取更新后的 DOM

当前问题卡在第一步。状态修改没有进入原来依赖的响应式对象,后面增加等待无法补回这次触发。

为了让页面刷新而额外增加一个 watch,也会让数据流绕得更远。重置操作已经明确知道何时发生,优先在这个操作中更新正确的状态来源,会比再建立一层监听更容易维护。

八、表单重置时可以按什么顺序排查

遇到数据看起来已经变了,页面却没有更新这类情况的时候,可以先把表单的状态流写清楚:

  • 默认数据从哪里创建
  • 接口结果写入哪个对象
  • 模板读取哪个对象
  • 重置函数修改哪个对象
  • 是否替换了根引用

第一步,检查表单最初由 reactive 还是 ref 创建。

ts 复制代码
const form = reactive(defaultForm)

通常预期后续修改属性。

ts 复制代码
const form = ref(defaultForm)

可以通过修改 form.value 替换整份数据。

第二步,检查重置函数是否重新赋值了保存 reactive 对象的局部变量:

ts 复制代码
let form = reactive(defaultForm)

form = reactive(nextForm)

如果有这种写法,继续确认它是否依赖其他状态触发重新渲染。

第三步,检查默认对象是否每次重新创建。直接复用同一个可变默认对象,可能让上一次编辑污染下一次重置结果。

更稳妥的形式是:

ts 复制代码
function createDefaultForm() {
  return {
    name: '',
    department: ''
  }
}

每次调用都得到新的默认数据。

第四步,检查表单是否包含动态字段、嵌套引用或子组件本地副本。根对象重置正确,不代表这些额外状态会自动按业务要求清理。

第五步,再检查校验组件、上传组件或第三方表单库是否维护了独立状态。清空数据字段可能不会同时清除校验提示、文件队列和内部缓存,需要调用对应组件提供的重置能力。

九、当前处理方式还存在哪些边界

本文只讨论 Vue 3 Composition API 中的响应式对象重置,没有把第三方表单组件的行为算进 Vue 响应式规则。

如果项目使用表单库,它可能在响应式数据之外保存校验结果、初始快照或字段注册信息。修改 form 后页面值已经更新,错误提示仍然存在,需要继续检查表单实例的重置接口。

接口数据也可能缺少部分字段。使用 Object.assign(form, response) 时,响应中不存在的字段会保留旧值。编辑不同用户时,这可能让前一个用户的数据残留在表单中。比较稳妥的顺序通常是先恢复默认结构,再合并当前接口数据:

vue 复制代码
<script setup>
import { reactive } from 'vue'

function createDefaultForm() {
  return {
    name: '',
    department: '',
    remark: ''
  }
}

const form = reactive(createDefaultForm())

function fillForm(response) {
  Object.assign(
    form,
    createDefaultForm(),
    response
  )
}

fillForm({
  name: 'Alice',
  department: '前端'
})

console.log(form)
</script>

控制台结果为:

text 复制代码
{
  name: 'Alice',
  department: '前端',
  remark: ''
}

默认值先覆盖旧状态,接口数据再覆盖对应字段。这个顺序能处理当前固定字段表单,但仍然不会删除默认结构之外的动态属性。

较大的表单还要考虑性能、校验时机和组件拆分。是否使用 reactiveref 或多个独立状态,应根据表单更新方式和组件边界选择,不能只凭字段数量决定。

十、小结

这次页面问题出现在表单重置阶段。模板最初读取了一个 reactive 对象上的属性,重置函数却让普通局部变量指向了另一份响应式对象。旧对象的属性没有变化,局部变量赋值也没有单独安排组件更新,所以输入框仍可能显示旧值。

表单需要保持同一个根对象时,可以直接修改属性,或者使用 Object.assign 把默认值写回原响应式对象。业务需要频繁替换整份数据时,可以使用 ref,通过修改 .value 让根对象替换进入响应式更新流程。

这些方案不会自动清理动态字段、嵌套引用、校验状态和第三方组件内部数据。下次遇到页面没有跟着表单变化,可以先确认模板读取的是哪一份状态、重置函数修改的是属性还是根引用,再考虑 DOM 更新时机。下一篇会继续分析响应式对象解构后,为什么局部变量不会跟随原属性变化。

相关推荐
再吃一根胡萝卜4 小时前
08 · 前端:Vue3 + SSE 流式与执行链路可视化
面试
再吃一根胡萝卜4 小时前
03 · 后端:FastAPI 与分层架构
面试
mldong6 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排6 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60617 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角8 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!8 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰8 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
Interview Aid1129 小时前
Walmart Global Tech SDE 三轮面经|基础、并发、压力面
面试·职场和发展
mayaairi10 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js