Vue.js对象操作指南:响应式原理、安全操作与性能优化
发布时间:2026/8/13 7:55:26 作者:尧图编辑部 阅读量:1,286

1. 项目概述为什么Vue.js中的对象操作如此重要在Vue.js的日常开发中我们打交道最多的数据结构除了数组恐怕就是对象了。无论是从后端API接口接收到的JSON数据还是组件内部管理的状态对象都无处不在。然而很多开发者尤其是刚接触Vue的朋友常常会陷入一个误区把Vue中的对象当作普通的JavaScript对象来操作。结果就是明明数据已经更新了视图却纹丝不动或者触发了意料之外的副作用。我自己在带团队和做项目的过程中无数次看到类似的坑。这个问题的核心在于Vue.js的响应式系统是基于Object.definePropertyVue 2或ProxyVue 3实现的。这意味着当你把一个普通对象交给Vue的data选项或reactive函数时Vue会把它“包装”成一个响应式对象。你对这个对象的直接赋值、新增或删除属性等操作如果不遵循Vue的特定方式就可能“逃过”响应式系统的侦测。因此掌握Vue语境下对象的“正确打开方式”是写出稳定、可预测的Vue应用的基本功。本文将围绕几个最高频、也最容易出错的场景展开如何安全地获取对象的值和键、如何将对象转换为数组进行渲染、以及如何合并多个对象而不破坏响应性。我会结合Vue 2和Vue 3的不同特性给出具体的操作方法和背后的原理并分享大量从实际项目中总结出来的“避坑指南”。无论你是正在学习Vue的新手还是希望梳理最佳实践的老手这篇文章都能提供直接的参考。2. 核心思路理解Vue响应式对象的特殊性在深入具体操作之前我们必须先建立正确的认知Vue中的响应式对象不是普通的JavaScript对象。这个认知差异是决定后续所有操作是否正确的基石。2.1 Vue 2与Vue 3响应式原理的差异Vue 2使用Object.defineProperty来劫持对象的属性。这种方式有一个著名的限制它无法检测到对象属性的添加或删除。也就是说如果你有一个响应式对象obj直接执行obj.newKey ‘value’这个newKey不会是响应式的。同样使用delete obj.key删除属性视图也不会更新。为了解决这个问题Vue 2提供了专用的APIVue.set或this.$set和Vue.delete或this.$delete。Vue 3则使用了ES6的Proxy。Proxy可以拦截对象上几乎所有的操作包括属性的增删改查。因此在Vue 3的reactive对象上你可以像操作普通对象一样进行增删属性并且能保持响应性。这是一个巨大的进步简化了很多开发心智负担。但是这并不意味着你可以完全为所欲为后面我们会谈到一些Proxy下的注意事项。2.2 响应式对象的“身份”与引用另一个关键点是引用。当你从响应式对象中取出一个嵌套的对象或数组时它可能仍然是响应式的。例如// Vue 3 Composition API 示例 import { reactive } from ‘vue‘; const state reactive({ user: { name: ‘张三‘, address: { city: ‘北京‘ } }, list: [1, 2, 3] }); const innerUser state.user; // innerUser 仍然是一个响应式Proxy对象 const innerList state.list; // innerList 也仍然是响应式Proxy对象这意味着你对innerUser.name的修改同样会触发依赖state.user.name的视图更新。理解这一点对于管理数据流和避免不必要的渲染至关重要。2.3 操作的核心原则基于以上特性我们可以总结出在Vue中操作对象的核心原则优先使用Vue提供的方法在Vue 2中对于动态增删属性务必使用$set和$delete。明确操作意图你是想读取数据、转换数据结构还是修改源数据不同的意图对应不同的方法。保持响应性任何操作都不应意外地丢失数据的响应性除非你明确需要例如为了传递给一个不需要响应性的外部函数。性能考量对于大型对象或高频操作选择性能最优的方法。例如直接遍历Object.keys()可能比先转成数组再操作更高效。接下来的章节我们将把这些原则应用到每一个具体场景中。3. 场景一安全地获取键Key、值Value与键值对Entries这是最基础的操作但里面也有学问。我们通常需要获取对象的键名数组、值数组或者键值对数组用于v-for渲染、计算属性或者传递给其他函数。3.1 使用Object.keys(),Object.values(),Object.entries()这是ES6标准提供的方法在Vue中可以直接使用它们返回的是普通数组。const myObject reactive({ a: 1, b: 2, c: 3 }); // 获取所有键名 const keys Object.keys(myObject); // [‘a‘, ‘b‘, ‘c‘] // 获取所有值 const values Object.values(myObject); // [1, 2, 3] // 获取所有键值对数组形式 const entries Object.entries(myObject); // [[‘a‘, 1], [‘b‘, 2], [‘c‘, 3]]注意事项与实操心得响应性链接keys、values、entries这三个数组本身不是响应式的。它们是Object静态方法返回的快照。如果myObject的键值发生变化这三个数组不会自动更新。你需要将它们放在computed属性中或者在使用它们的组件/函数被重新执行时再次调用这些方法。在computed中使用这是最常见的模式确保派生数据随源数据更新。import { reactive, computed } from ‘vue‘; const state reactive({ a: 1, b: 2 }); const keyList computed(() Object.keys(state)); // 当state的键变化时keyList会自动更新在模板中直接使用你也可以在模板的插值或v-for中直接调用这些方法。Vue的模板表达式会在每次渲染时重新求值因此也能得到最新结果但要注意性能如果对象很大且渲染频繁更推荐使用computed。template div v-for“key in Object.keys(myObject)“ :key“key“ {{ key }}: {{ myObject[key] }} /div /templateVue 2的特殊情况对于Vue 2如果你用Object.keys()去遍历一个组件实例this你会得到很多Vue内部添加的属性和方法如$data,$emit。通常你需要的是this.$data的键。更常见的做法是直接遍历你定义的data对象。3.2 使用for...in循环for...in循环是遍历对象可枚举属性的经典方法。const obj reactive({ a: 1, b: 2 }); for (let key in obj) { console.log(key, obj[key]); // 依次输出 ‘a‘ 1, ‘b‘ 2 }注意事项与实操心得遍历原型链for...in会遍历对象自身和其原型链上的所有可枚举属性。如果你处理的是纯数据对象例如来自JSON API这通常不是问题。但如果你给对象原型添加了方法它们也会被遍历出来。可以使用Object.hasOwnProperty()来过滤。for (let key in obj) { if (obj.hasOwnProperty(key)) { // 只处理对象自身的属性 } }顺序问题虽然现代JavaScript引擎通常按属性创建顺序维护键的迭代顺序对于整数键有特殊规则但for...in的迭代顺序在规范中并不绝对保证。如果顺序对你至关重要比如渲染列表更推荐先使用Object.keys()获取数组然后对数组进行排序或操作。性能对于非常大的对象for...in可能比先调用Object.keys()再遍历数组稍慢因为后者只需要一次属性收集。但在绝大多数业务场景下差异可以忽略不计。3.3 获取单个属性值点符号与方括号符号这看似简单但在Vue的响应式系统中有一个重要细节。const obj reactive({ ‘user-name‘: ‘alice‘, normalKey: ‘bob‘ }); // 点符号适用于有效的标识符属性名 const name1 obj.normalKey; // ‘bob‘ // 方括号符号适用于任何字符串作为属性名特别是包含连字符、空格或动态属性名 const name2 obj[‘user-name‘]; // ‘alice‘ const dynamicKey ‘user-name‘; const name3 obj[dynamicKey]; // ‘alice‘注意事项与实操心得响应性追踪Vue会追踪你在模板、计算属性、侦听器或渲染函数中访问的响应式属性。无论是使用.还是[]只要访问路径是静态的在编译时可知Vue都能建立依赖关系。例如在computed(() obj.normalKey)中Vue知道它依赖obj.normalKey。动态属性名的挑战如果你使用一个变量作为键去访问属性例如obj[someDynamicKey]Vue在someDynamicKey变化时无法自动建立新的依赖。这意味着如果someDynamicKey从‘a‘变为‘b‘计算属性或侦听器可能不会重新执行因为它之前追踪的是obj[‘a‘]而不是obj[‘b‘]。解决方法是使用计算属性返回一个函数或者确保在依赖收集的上下文中访问所有可能的键。// 不太理想watch无法深度追踪dynamicKey变化带来的影响 const dynamicKey ref(‘a‘); watch(() obj[dynamicKey.value], (newVal) { ... }); // 更好的做法使用计算属性包装或者watch源对象和键 const targetValue computed(() obj[dynamicKey.value]); watch(targetValue, (newVal) { ... }); // 现在能正确响应dynamicKey的变化4. 场景二将对象转换为数组进行渲染与处理在Vue模板中v-for指令主要用于遍历数组。当我们的数据源是一个对象但希望以列表形式渲染时就需要进行转换。此外数组拥有map、filter、reduce等强大的方法有时将对象转为数组处理后再转回去逻辑会更清晰。4.1 转换的目的与方法转换的目的通常有两个为了渲染v-for将对象{id1: item1, id2: item2}转换为数组[item1, item2]或[{id: ‘id1‘, data: item1}, ...]以便遍历。为了数据处理利用数组方法进行过滤、映射、排序等复杂操作。核心方法依然是Object.values(),Object.entries()和Object.keys()。const itemMap reactive({ 101: { name: ‘商品A‘, price: 100 }, 102: { name: ‘商品B‘, price: 200 }, }); // 方法1直接转换值数组如果只需要值 const itemListForRender computed(() Object.values(itemMap)); // 输出: [{name:‘商品A‘,price:100}, {name:‘商品B‘,price:200}] // 方法2转换键值对保留ID信息更常见 const itemListWithId computed(() Object.entries(itemMap).map(([id, item]) ({ id, ...item, // 使用扩展运算符将item的属性展开 })) ); // 输出: [{id:‘101‘, name:‘商品A‘,price:100}, {id:‘102‘, name:‘商品B‘,price:200}]在模板中使用template !-- 使用方法1的数组 -- ul li v-for“(item, index) in itemListForRender“ :key“index“ {{ item.name }} - {{ item.price }} /li /ul !-- 使用方法2的数组key使用id更稳定 -- ul li v-for“item in itemListWithId“ :key“item.id“ {{ item.name }} - {{ item.price }} /li /ul /template4.2 使用Array.from()进行转换Array.from()方法可以将一个可迭代或类数组对象转换为真正的数组。虽然对象本身不是可迭代的但Object.keys(obj)等返回的是数组所以通常不直接用于对象。但它可以用于转换Object.entries()的结果或者处理其他场景。const obj { a: 1, b: 2 }; // 这其实多此一举因为Object.entries已经返回数组了 const entriesArray Array.from(Object.entries(obj)); // [[‘a‘, 1], [‘b‘, 2]]Array.from()更强大的地方在于其第二个参数mapFn它允许你在转换过程中对每个元素进行映射。const itemMap { 101: ‘商品A‘, 102: ‘商品B‘ }; const itemList Array.from( Object.entries(itemMap), ([id, name]) ({ id, name }) // 直接在此处完成映射 ); // 输出: [{id:‘101‘, name:‘商品A‘}, {id:‘102‘, name:‘商品B‘}]4.3 转换过程中的性能与响应性保持注意事项与实操心得计算属性的缓存务必使用computed来包装转换逻辑。computed会缓存结果只有当依赖的响应式对象itemMap发生变化时才会重新计算转换后的数组。如果在模板或方法中直接调用Object.values(itemMap)每次渲染都会执行一次转换对于大型对象会有性能损耗。避免不必要的转换如果你的操作只是查找某个键的值或者判断键是否存在直接在原对象上操作obj[key]或key in obj比先转换成数组再查找要高效得多。深层次转换如果对象的值本身也是对象或数组Object.values()返回的数组中的元素引用的是原始响应式对象的属性值。在Vue 3中如果原始值是响应式的通过reactive包装那么数组中的这个元素也保持响应式。在Vue 2中情况类似但要注意Vue 2对数组嵌套对象的侦测需要依靠Vue.set或数组的变异方法。排序需求对象属性的枚举顺序虽然有一定规则但如果你需要特定的排序如按值中的某个字段排序应该在转换后的数组上使用sort方法。const sortedList computed(() { const list Object.values(itemMap); return list.sort((a, b) a.price - b.price); // 按价格升序 });5. 场景三对象的合并、扩展与更新合并多个对象或者用新数据更新现有对象是业务中的高频操作。例如更新用户资料、合并查询参数、初始化状态等。这里面的坑最多一不小心就会破坏响应性。5.1 浅合并与深合并首先需要明确需求是浅合并只合并第一层属性还是深合并递归合并所有嵌套属性1. 使用扩展运算符...(Spread Operator) 进行浅合并const obj1 reactive({ a: 1, b: { x: 10 } }); const obj2 { b: { y: 20 }, c: 3 }; // 浅合并后面的对象属性会覆盖前面的 const mergedShallow { ...obj1, ...obj2 }; console.log(mergedShallow); // 输出: { a: 1, b: { y: 20 }, c: 3 } // 注意obj1.b.x 被 obj2.b 整个覆盖了而不是合并 {x:10, y:20}2. 使用Object.assign()进行浅合并const mergedByAssign Object.assign({}, obj1, obj2); // 结果与扩展运算符相同Object.assign()会直接修改第一个目标对象这里是空对象{}所以通常我们传入一个空对象作为目标以避免污染源对象。3. 深合并的需求与实现浅合并无法满足嵌套对象的合并需求。我们需要深合并。Vue本身没有提供深合并工具我们需要自己实现或使用第三方库。手动递归实现适用于简单场景function deepMerge(target, source) { for (let key in source) { if (source[key] typeof source[key] ‘object‘ !Array.isArray(source[key])) { // 如果是对象且不是数组则递归合并 if (!target[key] || typeof target[key] ! ‘object‘) { target[key] {}; } deepMerge(target[key], source[key]); } else { // 基本类型或数组直接覆盖数组通常也是整体覆盖而非合并 target[key] source[key]; } } return target; } const objA reactive({ a: 1, b: { x: 10, z: 30 } }); const objB { b: { y: 20 }, c: 3 }; deepMerge(objA, objB); console.log(objA); // { a:1, b:{ x:10, y:20, z:30 }, c:3 }使用第三方库lodash.merge或deepmerge是社区广泛使用的、功能更完善的深合并库。5.2 在Vue中安全地合并响应式对象这是最关键的部分。我们的目标不仅是合并数据还要确保合并后的结果保持响应性并且合并操作本身能被Vue正确追踪。Vue 3中的最佳实践在Vue 3的Composition API中推荐使用reactive包裹一个对象然后直接对其属性进行赋值。对于合并可以这样做import { reactive } from ‘vue‘; const state reactive({ user: { name: ‘老张‘, age: 30 }, settings: { theme: ‘light‘ } }); // 假设从API获取到部分更新数据 const updateData { user: { age: 31 // 只更新年龄 }, settings: { theme: ‘dark‘ } }; // 方法1逐个属性赋值最安全响应性有保障 Object.keys(updateData).forEach(key { if (state[key] typeof state[key] ‘object‘ typeof updateData[key] ‘object‘) { // 如果是对象则合并这里用浅合并举例 Object.assign(state[key], updateData[key]); } else { // 基本类型或替换整个对象 state[key] updateData[key]; } }); // 方法2使用扩展运算符创建新对象再整体替换适用于顶层属性 // 注意这会替换整个state对象所有依赖state的引用都需要更新不推荐轻易使用。 // state reactive({ ...state, ...updateData }); // 错误会丢失响应性连接 // 正确做法是替换state的属性而不是state本身 state.user { ...state.user, ...updateData.user }; state.settings { ...state.settings, ...updateData.settings };Vue 2中的最佳实践在Vue 2中由于Object.defineProperty的限制动态添加根级属性必须使用Vue.set或this.$set。对于合并操作// 在Vue组件方法中 updateState(newData) { for (const key in newData) { if (this.state.hasOwnProperty(key)) { // 如果state已有该属性可以直接赋值对于对象Vue能检测到内部变化 if (typeof this.state[key] ‘object‘ typeof newData[key] ‘object‘) { Object.assign(this.state[key], newData[key]); } else { this.state[key] newData[key]; } } else { // 如果state没有该属性必须使用$set使其响应式 this.$set(this.state, key, newData[key]); } } }注意事项与实操心得“响应式丢失”陷阱在Vue 3中如果你用let state reactive({})然后执行state reactive({...state, ...newData})你将完全替换state变量指向的响应式对象。之前所有引用这个旧state的computed、watch或模板将仍然依赖旧对象而新对象的变化不会被侦测到。这是最常见的错误之一。永远记住reactive()返回的是一个Proxy包装后的对象变量state存储的是对这个Proxy的引用。改变这个引用就断开了所有已有的响应式连接。数组的合并对象的数组合并逻辑需要特别定义。通常数组合并不是拼接而是根据某个键如id进行合并更新。这需要自定义逻辑例如使用find和map。性能考虑深合并尤其是递归合并大型对象是昂贵的操作。如果API返回的数据结构扁平或者你明确知道需要更新的字段直接赋值比深合并更高效。在watch或组件更新钩子中进行合并操作时要小心性能问题。使用ref与reactive的取舍对于顶层的状态有时使用ref包裹一个对象比用reactive更灵活。因为ref的.value可以被整体替换而依赖关系仍然通过ref本身建立。例如const state ref({}); state.value { ...state.value, ...newData };这样是安全的因为computed或watch依赖的是state这个ref而不是它内部.value的具体对象。6. 场景四其他实用操作与技巧除了上述三大场景还有一些零散但非常实用的对象操作技巧。6.1 判断属性是否存在in,hasOwnProperty, 与Reflect.hasconst obj reactive({ a: 1, b: undefined }); // 方法1: in 操作符 (检查原型链) console.log(‘a‘ in obj); // true console.log(‘b‘ in obj); // true (即使值为undefined) console.log(‘toString‘ in obj); // true (来自Object.prototype) // 方法2: Object.prototype.hasOwnProperty (只检查自身属性) console.log(obj.hasOwnProperty(‘a‘)); // true console.log(obj.hasOwnProperty(‘toString‘)); // false // 方法3: Reflect.has (功能同 in 操作符) console.log(Reflect.has(obj, ‘a‘)); // true选择建议大多数情况下如果你只关心对象自身定义的属性不包含继承的使用hasOwnProperty。如果需要检查一个属性是否存在包括可能从原型继承的使用in或Reflect.has。在Vue的响应式上下文中这些检查都能被正确追踪。6.2 删除属性delete操作符与Vue.delete/Reflect.deleteProperty// Vue 3 (使用Proxydelete操作符可以工作) const objV3 reactive({ a: 1, b: 2 }); delete objV3.a; // 正确响应式系统能检测到 console.log(‘a‘ in objV3); // false // Vue 2 (必须使用Vue.delete或this.$set来保持响应性) // 在组件内 this.$delete(this.objV2, ‘a‘); // 全局 Vue.delete(this.objV2, ‘a‘); // 使用Reflect.deleteProperty (标准方法在Vue 3中也可用) Reflect.deleteProperty(objV3, ‘b‘);关键点在Vue 2中绝对不能使用普通的delete操作符删除响应式对象的属性否则会导致该属性变更无法触发视图更新。必须使用Vue.delete。在Vue 3中由于Proxy的支持delete操作符可以正常工作。6.3 冻结与密封Object.freeze与Object.seal有时你可能希望防止响应式对象被修改。Object.freeze(obj): 冻结对象不能添加、删除、修改其属性。在Vue中冻结一个对象意味着Vue无法为其建立响应式追踪。如果你有一个永远不会变的大型配置对象冻结它可以带来轻微的性能提升。Object.seal(obj): 密封对象可以修改现有属性但不能添加或删除属性。注意在Vue 3的reactive()中传入一个已冻结的对象会直接返回原对象因为无法被Proxy代理。在Vue 2中被冻结对象的属性变化同样无法被侦测。6.4 使用toRef和toRefs解构响应式对象这是Vue 3 Composition API中极具价值的工具函数用于在保持响应性的前提下解构对象。import { reactive, toRef, toRefs } from ‘vue‘; const state reactive({ name: ‘Vue‘, version: 3 }); // 直接解构会丢失响应性 const { name, version } state; // name和version现在是普通的字符串 // 使用toRef为单个属性创建ref const nameRef toRef(state, ‘name‘); // 响应式修改state.name会同步 // 使用toRefs为所有属性创建ref const { name: reactiveName, version: reactiveVersion } toRefs(state); // reactiveName和reactiveVersion都是ref修改它们等价于修改state.name/state.version使用场景当你需要将响应式对象的属性作为独立的ref传递给组合式函数或模板时toRefs非常有用。它允许你在不丢失响应性的情况下使用ES6解构语法。7. 常见问题排查与性能优化实录在实际项目中对象操作引发的bug和性能问题屡见不鲜。这里记录几个典型案例和解决方案。7.1 问题视图不更新但控制台打印数据已变症状在方法中修改了对象属性控制台console.log显示数据已更新但页面上绑定的视图没有任何变化。排查步骤与解决检查对象是否是响应式的在Vue 3中确认对象是用reactive()或ref()创建的。在Vue 2中确认对象是在组件的data函数中返回的。一个简单的检查方法是在模板或计算属性中访问该属性如果Vue Devtools能追踪到它就是响应式的。检查属性添加方式Vue 2专属如果你在Vue 2中向一个已创建的响应式对象添加了新的根级属性并且使用的是obj.newKey value这就是问题所在。必须使用this.$set(this.obj, ‘newKey‘, value)。检查数组索引赋值Vue 2专属在Vue 2中直接通过索引设置数组项如this.items[index] newValue也是无法被侦测的。应使用this.$set(this.items, index, newValue)或数组的splice方法this.items.splice(index, 1, newValue)。检查异步更新队列Vue的DOM更新是异步的。如果你在同一个事件循环中修改数据后立即读取DOM得到的是旧值。可以使用this.$nextTick(callback)确保在DOM更新后执行代码。检查引用是否被替换在Vue 3中你是否不小心替换了整个reactive对象记住reactive()返回的Proxy引用必须保持不变。7.2 问题深层次嵌套对象修改视图更新不完整症状修改了一个深层嵌套的对象属性如obj.a.b.c 10只有直接绑定c的视图更新了绑定obj.a.b或obj.a的视图没有更新。分析与解决这在Vue 2和Vue 3中都是正常行为。Vue的响应式系统追踪的是属性的访问。如果一个组件或计算属性只使用了obj.a.b.c那么它只依赖于c。当c变化时它会更新。如果它没有访问obj.a.b这个中间对象那么obj.a.b的变化就不会触发它更新。这其实是Vue的一种性能优化细粒度追踪。如果你需要让某个组件对嵌套对象内部任何变化都做出反应有几种策略使用深度侦听器watchwatch(() state.someNestedObject, (newVal) { … }, { deep: true })。但深度侦听开销较大需谨慎使用。扁平化状态结构如果可能尽量避免过深的嵌套。将频繁变化的属性提升到更浅的层级。使用计算属性返回新引用对于需要依赖整个嵌套对象的情况可以创建一个计算属性返回该嵌套对象的一个新引用例如使用JSON.parse(JSON.stringify(...))或扩展运算符进行浅克隆。但这会破坏响应式链接且性能不佳仅作为最后手段。7.3 性能优化避免在模板或渲染函数中进行重型对象操作反例template div v-for“item in Object.values(hugeObject).filter(i i.active)“ :key“item.id“ {{ item.name }} /div /template每次渲染这个组件都会对hugeObject执行Object.values和filter操作。如果对象很大或组件渲染频繁这将严重消耗性能。正例import { computed } from ‘vue‘; // 在setup或computed选项中 const activeItems computed(() Object.values(hugeObject).filter(i i.active) );template div v-for“item in activeItems“ :key“item.id“ {{ item.name }} /div /template使用computed属性转换和过滤操作只会在hugeObject或其内部active属性发生变化时执行结果会被缓存极大提升性能。7.4 一个关于对象合并的典型坑响应式污染场景你从某个外部库或函数接收了一个普通对象plainObj然后你想把它合并到你的响应式状态state中。错误做法const externalData getPlainDataFromAPI(); // 返回普通对象 state.someField externalData; // 或 Object.assign(state, externalData)如果externalData是一个复杂的、嵌套很深的对象直接赋值会将一个非响应式的对象引入到你的响应式系统中。此后修改externalData内部的嵌套属性Vue将无法侦测。正确做法确保合并进来的数据也是响应式的。// Vue 3 import { reactive } from ‘vue‘; const externalData getPlainDataFromAPI(); // 方案1如果externalData是顶层属性 state.someField reactive(externalData); // 方案2如果是要合并到现有响应式对象的属性中且需要深合并 // 使用一个工具函数在合并过程中确保新值的响应性 function reactiveMerge(target, source) { for (const key in source) { if (source[key] typeof source[key] ‘object‘) { if (!target[key] || typeof target[key] ! ‘object‘) { target[key] reactive({}); } reactiveMerge(target[key], source[key]); } else { target[key] source[key]; } } } reactiveMerge(state, externalData);核心原则维持响应式数据边界的清晰。从外部进入响应式系统的数据应尽早将其“响应式化”。