news 2026/9/10 1:37:33

Vue响应式原理深度拆解:Vue2与Vue3实现对比及面试实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vue响应式原理深度拆解:Vue2与Vue3实现对比及面试实战指南

用了很久 Vue,真正把它当成黑盒子去用的开发者其实不少。写业务的时候,数据一变页面就跟着变,好像很自然,但一旦有人问你"Vue 数据响应式原理到底是什么",十个人里有七八个会卡壳。尤其是面试冲刺阶段,这个点几乎是必考的大题。你要是能把数据从变更到视图更新的完整链路讲清楚,再随手写一个极简响应式实现,面试官基本就能确定你的 Vue 底子是够的。这篇我打算从源码角度和工程实践两个层面,把 Vue 2 和 Vue 3 的响应式实现一起拆开讲,顺带把开发中经常碰到"数据改了页面不更新"这类问题给一并解决掉。

这篇文章适合三类人看:准备前端面试、正在背 Vue 原理题的人;项目里用过 Vue 但遇到数据更新异常、只会用this.$set硬凑的人;以及想从 Vue 2 平滑过渡到 Vue 3,想搞清楚reactive/ref/computed到底怎么运作的人。我会从设计思路讲到源码细节,再给一套可以直接抄的排查路径,尽量做到不只让你"知道是什么",还让你"知道为什么"。

1. 响应式机制的整体设计思路

1.1 一句话理解"响应式"

响应式的本质,用大白话说就是:数据变化了,所有依赖这份数据的地方会自动跟着更新。这里的"地方"包括模板渲染结果、computed计算属性、watch侦听器,以及你手动注册的watchEffect回调。

我经常用表格公式来打比方:你在 Excel 的 A1 单元格里填了 10,B1 填了公式=A1*2,这时候 B1 显示 20。当你把 A1 改成 20,B1 不用你管,自己就变成了 40。Vue 的响应式系统就是这套机制,只不过把"单元格"换成了 JavaScript 对象里的属性,把"公式"换成了渲染函数和侦听器回调。核心要解决的问题有两个:一个是"谁依赖了这个数据",另一个是"数据变了,怎么告诉依赖方"。

这两个问题对应到 Vue 源码里,就是依赖收集(track)和触发更新(trigger)。依赖收集发生在"读"数据的时候,触发更新发生在"写"数据的时候。你读了一个响应式属性,Vue 就把当前的"订阅者"记下来;你改了它,Vue 就把记下来的订阅者全部找出来,挨个通知。

1.2 发布订阅模型:Dep 和 Watcher 怎么配合

订阅者是谁?在 Vue 2 里叫Watcher组件会创建一个render watchercomputed会创建computed watcherwatch选项会创建user watcher。它们自己维护一个回调,收到通知后分别去重新渲染、重新计算、或者执行你的处理函数。

负责记录和派发的是Dep,全称 Dependency,你可以把它理解成一个"依赖管理小盒子"。每个响应式属性在被访问时,会把当前的Watcher收进盒子里;属性被改写时,打开盒子把里面所有Watcher逐个拿出来调用update()方法。这里的模式就是经典的发布订阅,数据源是发布者,视图和副作用函数是订阅者,Dep是中间的消息中心。

有一个容易忽视的细节:Watcher在被创建的时候,会把自己临时挂到一个全局变量(Vue 2 里是Dep.target)上,然后立刻去"读"一次数据。这样在 getter 拦截器里,Dep.target肯定指向当前 watcher,于是就能正确地把依赖关系建立起来。等到 watcher 创建完,再把Dep.target置空。"先读一次数据"这个步骤很关键,它就是 Vue 自动收集依赖的入口。

1.3 Vue 2 和 Vue 3 实现方式为什么完全不一样

Vue 2 用的是Object.defineProperty,把对象属性一个个用 getter/setter 包起来。Vue 3 用的是Proxy,把整个对象包一层代理。看起来只是 API 差异,实际上对应了两种能力边界。

Object.defineProperty一次只能处理一个属性,所以 Vue 2 需要递归遍历对象,把所有属性全部改写。它天生拦截不了"新增属性"和"删除属性",也拦截不了"通过下标修改数组项"这类操作。于是 Vue 2 只能额外提供Vue.set/Vue.delete/$set/$delete来补漏,并且通过改写数组方法的方式,让pushsplice这些方法在修改数组后主动通知更新。

Proxy不需要关心属性存在与否,它代理的是整个对象的读取、写入、删除、遍历等操作,天然覆盖了新增和删除。而且Proxy的 get 拦截器可以在访问到深层对象时才去代理那一层,实现"懒代理",初始化性能比 Vue 2 那种一上来就把整棵树递归完的方式好很多。

这也解释了为什么 Vue 3 的reactive根本不需要 $set。因为你在reactive对象上新增一个属性,走的是set拦截器,trigger会被触发,依赖方正常更新,一个额外 API 都不需要。理解了这个差异,你就能明白后面很多"为什么"。

2. Vue 2 响应式实现细节

2.1 defineProperty 如何拦截属性读写

Vue 2 响应式的基石,是把普通属性改写成"访问器属性"。一张代码图就能说明问题:

function defineReactive(obj, key, val) { // 递归处理 val,保证嵌套对象也是响应式的 observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get() { // 读取时做依赖收集 if (Dep.target) { dep.depend() } return val }, set(newVal) { if (newVal === val) return val = newVal // 新值也要变成响应式 observe(newVal) // 写入时派发更新 dep.notify() } }) }

这里有一个关键点:闭包里的val才是真正存储数据的位置,外界读写obj[key],其实都是在跟这个闭包变量打交道。getter 里返回val,setter 里把newVal赋给val,然后触发通知。

dep.depend()做的事情,是把当前正在收集依赖的Watcher加进这个属性的依赖列表。dep.notify()做的事情,是把列表里的每个Watcher都调一遍update()。整个链路从render watcher读取属性开始,到属性被修改触发notify结束,形成闭环。

注意:只拦截当前对象这一层还不够。如果你的数据是{ user: { name: '张三' } },那么user这个属性的 setter 只负责监听user被整体替换,user.name的读写要由user对象自己的defineReactive来处理。这就是为什么要递归observe

2.2 Observer、Dep、Watcher 三个角色的协作

完整过程的角色可以这样分:

  • Observer:把一个对象变成响应式,遍历所有属性挨个执行defineReactive,同时给数组做方法重写。
  • Dep:每个响应式属性一个,负责收集和通知依赖。
  • Watcher:一个 Watcher 代表一个需要被通知的"订阅者"。

三者启动顺序是:initState阶段调用observe(data),创建Observer实例,完成属性重写;接着组件挂载时创建render watcherrender watcher会立刻执行渲染函数,边渲染边访问data里的属性,触发 getter 完成依赖收集。之后的数据变更触发 setter,dep.notify()render watcher叫醒,watcher 再调update(),最后走patch去更新真实 DOM。

我见过不少人把watcher.update()理解为"直接就重新渲染了"。实际上 Vue 2 里还有个调度器,watcher不会立刻执行,而是被推进一个异步队列,等本轮事件循环末尾统一去重执行,这就是nextTick机制的来源。也就是说,响应式系统负责通知,异步批处理负责优化,两者是分层的。

2.3 数组响应式的"特殊照顾"

数组是 Vue 2 响应式里最让人头大的部分。Object.defineProperty不是不能监听下标,而是 Vue 官方为了保证性能,只对能改变数组本身的方法做了拦截。源码里有一段重写数组原型的关键逻辑:

const arrayProto = Array.prototype export const arrayMethods = Object.create(arrayProto) const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] methodsToPatch.forEach(function (method) { const original = arrayProto[method] Object.defineProperty(arrayMethods, method, { enumerable: false, configurable: true, writable: true, value: function (...args) { const result = original.apply(this, args) const ob = this.__ob__ let inserted switch (method) { case 'push': case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break } if (inserted) ob.observeArray(inserted) ob.dep.notify() return result } }) })

这里比较核心的是this.__ob__,这是每个响应式对象/数组身上都挂的Observer实例,里面存着这个数组自己的dep。只要数组变更了,就调用ob.dep.notify()通知订阅者。同时,新插入的元素也要继续做响应式处理,所以push/unshift/splice的参数还会再走一遍observeArray

所以 Vue 2 数组响应式的核心结论是:pushpopshiftunshiftsplicesortreverse修改数组,视图能更新;用arr[0] = xxx改下标、用arr.length = 0清空数组,不能触发响应式更新。前者因为走的是被重写的方法,后者因为根本没有被拦截。

2.4 为什么要有 Vue.set / this.$set

缺失的属性没有 getter/setter,新增属性自然无法触发更新,所以 Vue 2 提供了Vue.set(object, key, value)来"手动增加响应式属性"。源码里的逻辑大致是:目标对象已经是响应式,就用defineReactive给新属性补上响应式能力,再手动调用ob.dep.notify()通知一次更新。

同理,Vue.delete要解决的是delete obj.prop之后依赖没有收到通知的问题。但这里要提醒一句:能不手动新增属性就尽量别新增。新增属性本身意味着数据结构在设计时没有考虑完整,正确做法是把所有字段在一开始就声明出来,哪怕是空值。这样做既省了 $set 的额外通知,也能避免因为$set只会触发该属性自己 dep 的通知,而出现某些依赖没被通知到的边界情况。

3. Vue 3 响应式实现细节

3.1 Proxy 和 Reflect 的基础用法

Vue 3 的reactive本质上就是new Proxy(target, handlers)。一个最基础的代理长这样:

function reactive(target) { if (typeof target !== 'object' || target === null) { return target } const proxy = new Proxy(target, { get(target, key, receiver) { const res = Reflect.get(target, key, receiver) track(target, key) return res }, set(target, key, value, receiver) { const oldValue = target[key] const result = Reflect.set(target, key, value, receiver) if (oldValue !== value) { trigger(target, key) } return result }, deleteProperty(target, key) { const hadKey = Object.prototype.hasOwnProperty.call(target, key) const result = Reflect.deleteProperty(target, key) if (hadKey && result) { trigger(target, key) } return result } }) return proxy }

为什么要用Reflect?因为Reflect.getReflect.set可以帮你在真实对象上执行操作,并且返回值语义更清晰:Reflect.set返回布尔值表示是否成功,Reflect.deleteProperty同样返回布尔值。另一个重要原因是receiver参数。如果对象的 getter 或 setter 里还访问了this,直接操作原始对象可能绕开代理,但通过Reflect.get(target, key, receiver)可以确保this指向代理本身,保证拦截器始终生效。这类问题在处理继承、computed内部读取this的时候会特别明显。

3.2 WeakMap 里到底存了什么

Vue 3 的依赖收集不是靠每个属性一个 Dep,而是一个中心化的数据结构。源码里的targetMap定义是:

const targetMap = new WeakMap() function track(target, key) { if (!activeEffect) return let depsMap = targetMap.get(target) if (!depsMap) { depsMap = new Map() targetMap.set(target, depsMap) } let dep = depsMap.get(key) if (!dep) { dep = new Set() depsMap.set(key, dep) } if (!dep.has(activeEffect)) { dep.add(activeEffect) activeEffect.deps.push(dep) } }

结构是这样的:WeakMap的键是原始对象,值是一个Map;这个Map的键是对象上的属性名,值是一个Set,里面存的是effect函数。所以依赖收集的整体逻辑可以理解为:先找到对象,再找到属性,再记录哪些副作用函数依赖了它。

WeakMap而不是普通Map的考虑很实际:WeakMap的键是弱引用,如果用户把响应式对象置空、不再引用,这个对象及其依赖数据可以被垃圾回收,不会造成内存泄漏。这个点在长页面、频繁创建临时响应式对象的场景里值得留意。

3.3 effect 和 activeEffect 的工作方式

Vue 3 把"副作用函数"抽象成了effectcomputedwatchEffectwatch、组件渲染,底层都是 effect 的封装。当一个 effect 开始执行时,会设置activeEffect指向自身,然后调用用户函数;函数内部访问响应式属性,就触发track,把activeEffect记录下来。执行完后恢复activeEffect

触发的代码长这样:

function trigger(target, key) { const depsMap = targetMap.get(target) if (!depsMap) return const dep = depsMap.get(key) if (!dep) return const effects = new Set(dep) effects.forEach(effect => { if (effect !== activeEffect) { effect.run() } }) }

effect.run()的具体行为还要看 scheduler 的配置。如果effect配置了scheduler,数据变化时执行的是 scheduler 而不是直接跑用户函数。组件渲染函数的 effect 就配置了 scheduler,会走异步队列批处理;watchEffect默认也是异步调度。这个设计保证了多次同步修改数据时,副作用不会同步执行 N 次,而是合并到一次更新中。

3.4 reactive、ref、computed 的区别与联系

reactive只能处理对象类型,普通基础类型(数字、字符串、布尔值)没有办法直接做代理,因为 Proxy 的 target 必须是对象。于是 Vue 3 提供了ref,本质上是用一个RefImpl类把基础值包成对象,然后在访问器value上做依赖收集。

ref(0)的简化实现思路:

class RefImpl { constructor(value) { this._value = value } get value() { track(this, 'value') return this._value } set value(newVal) { if (newVal !== this._value) { this._value = newVal trigger(this, 'value') } } }

所以ref在模板里自动解包,是因为渲染的 effect 在读取.value时建立了依赖;在原生 JS 里必须写.value才能读到你想要的值。reactive对象里的 ref 也会被自动解包,这是官方在 getter 里做了一层处理。但要注意数组里的 ref 不会自动解包,这个坑我在项目里踩过,reactive({ list: [ref(1), ref(2)] })里拿到的list[0]是 ref 对象本身,不是值 1。

computed是组合effect和懒惰执行的结果。它创建了一个自己的 ref,但同时注册了一个懒 effect——只有访问computed.value时,effect 才会被触发执行,如果依赖的数据没有变化,后续访问直接返回缓存的旧值。如果你在 effect 里访问了 computed,那么 computed 会把自己作为依赖再收集给上游,实现链式更新。这条链路是:数据变 → computed 的 effect 标记为 dirty → 访问 computed.value 时重新计算 → 依赖 computed 的渲染 effect 收到更新通知。

4. 高频面试题与排查技巧实录

4.1 为什么 watch 数组第一项新旧值一样

这个题在热词列表里出现了,它是一个非常典型的 "理解了依赖收集就懂了" 的问题。在 Vue 2 中,写watch: { list(newVal, oldVal) { ... } },当修改list[0].name时,你会发现newVal === oldVal,而且打印内容都是修改后的值。

原因是:watch默认监听的是数组的引用。修改list[0].name并没有替换list这个数组的引用,数组还是原来那个数组,所以新旧值指向同一份数据。虽然响应式系统的确触发了 watcher 回调,因为数组里某个属性依赖了这个 watcher,但 Vue 2 的 watch 并不会对数组内部做深拷贝快照。它把"旧值"和"新值"都简单地指向了vm.list,所以输出自然是同一个引用。

解决这个问题有几种做法:想拿到真正修改前的旧值,要么配置deep: true并配合一个handler,但还是拿不到前一个状态;要么手动做快照,比如watch的 getter 返回JSON.parse(JSON.stringify(this.list))。更实用的是在watch回调里只关注变化本身,不去依赖新旧值对比。Vue 3 里同理,watch默认也是浅对比旧引用的快照,深层变化拿不到旧值。

4.2 数组下标赋值不更新怎么办

这是 Vue 2 最经典的问题:this.arr[0] = '新值'页面没反应。原因前面说过,下标赋值没有走被重写的数组方法,也没有触发 setter。常见解决方案是改用this.$set(this.arr, 0, '新值'),或者干脆用this.arr.splice(0, 1, '新值')

在 Vue 3 里,这个限制不存在了。Proxy 的set拦截器能捕获到下标赋值,即使属性本来不存在也照样可以 track 和 trigger。在实际项目中,如果还在维护 Vue 2 老项目,建议写一套统一的列表更新方法,比如updateItem(index, value),内部统一走$setsplice,避免团队里每个人踩一遍同样的坑。

4.3 对象新增属性不更新的排查

Vue 2 中,this.obj.newKey = 'value'后页面不更新,这是响应式系统的已知限制。标准做法是this.$set(this.obj, 'newKey', 'value'),或者在初始化时就声明好全部字段。需要特别提醒的是,嵌套层面的$set不太好排查,例如this.obj.a.b.c = 1b本身是后来手动加上的,那c的改动永远不会被感知。这种问题建议直接在控制台打印this.obj,观察属性有没有getter/setter标记,如果新属性没有标记,说明它根本没进入响应式系统。

Vue 3 中,reactive对象上新增属性直接能工作,这是 Proxy 的优势。但如果你用的是ref初始化基本类型,再去挂属性就不会有响应式,因为基本类型不可能是响应式对象。遇到"ref 对象挂了属性不更新"的问题时,先检查是不是用错了 API。

4.4 响应式丢失的三个典型场景

响应式丢失是 Vue 3 里一个比较隐蔽的问题,比 Vue 2 的 $set 坑更容易踩。第一个场景是解构:const { name } = reactive(obj),解构后name变成了普通字符串,后续修改obj.namename变量不会更新。第二个场景是赋值给新变量后直接改:let newObj = reactive(obj).someProp,修改newObj可能只会改到原始值。第三个场景是展开:const newObj = { ...obj },这也只是复制了当前值,不是一个响应式代理。

解决办法是把要注意响应性的值继续用reftoRefs包一层。toRefs把一个reactive对象的每个属性都变成ref,解构后仍然保持响应式。源码里toRefs的实现就是遍历对象,为每个 key 创建一个ObjectRefImpl,它的.valuegetter 实际上读的是原代理对象的对应属性,所以响应式链路没有断。

4.5 常见 Warning 快速排查表

这里把我自己在开发中频繁遇到的 Vue 相关报错和警告整理成一个速查表,每条都是真实踩过的坑:

报错 / 现象产生原因解决办法
[Vue warn]: Property "xxx" was accessed during render but is not defined on instance.模板里访问了实例上不存在的属性检查 data 里是否声明,或者拼写是否一致
[Vue warn]: Unknown custom element: <el-carousel-item>使用 Element UI 等组件库时,组件没有注册检查是否全局注册了组件,或者Vue.use()是否执行;局部导入时确认components配置
RangeError: Maximum call stack size exceeded可能是递归调用过深,也可能是响应式把循环引用的对象整个代理了检查代码中是否存在循环引用;Vue 3 中对循环引用对象做响应式可能递归爆栈
98% after emitting CopyPlugin启动卡住构建阶段内存不足或存在大量静态资源复制任务排查看是否是 node_modules 中的阻塞插件;增大 Node 内存NODE_OPTIONS=--max-old-space-size=4096;检查public目录是否过大
Vue warn]: Error in beforeCreate hook生命周期钩子里抛错,通常是数据或逻辑错误把钩子内代码包裹 try/catch,检查异步执行顺序
vue watch 数组第一项新旧值一样深层变更时引用相同用 getter 做快照,或关注变化过程而非新旧值对比

4.6 排查"数据变了但页面没更新"的标准流程

遇到过太多次这种问题,我现在的排查流程已经固定成了四步:

第一步,先确认数据真的变了。在修改处打印一下当前值,很多所谓"没更新"其实是因为异步逻辑修改了错误的引用,比如把值赋给了局部变量而不是this上的响应式字段。第二步,区分是 Vue 2 还是 Vue 3。Vue 2 需要考虑新增属性、下标赋值、数组方法这三大类限制;Vue 3 大部分情况考虑响应式丢失,比如解构、展开、赋值给新变量。第三步,检查是不是被其他因素覆盖了。比如computed没有 setter 然后你手动赋值,控制台会提示不要直接给 computed 赋值;或者你的修改被同步执行的其他逻辑又改了回去。第四步,查看浏览器的 Vue DevTools。Vue 2 里能清楚看到属性是否带响应式标记,Vue 3 的 DevTools 也支持查看reactiveref的内部值。

5. 响应式机制带来的实战编码规范

5.1 能用扁平结构就别用深嵌套

从响应式原理反推编码规范,最直接的一点就是:数据层级越深,响应式系统需要跟踪的依赖就越分散,出问题的地方也越多。Vue 2 里深嵌套对象会递归初始化全部属性,性能随着对象深度线性下降;Vue 3 虽然用了懒代理,深层组件里的深度响应式数据在调试时依然很难追踪。

我在实际项目中有一条铁律:列表数据一律扁平化,不要超过三层嵌套。一个后端返回的树形菜单,前端最好先转换成扁平数组结构,通过parentId建立关联,用computed再生成树。这样数据更新和查找都方便,响应式追踪的粒度也更清晰。

5.2 大数据列表的性能边界和优化

响应式系统在"大而深的数据"上会有性能压力。Vue 3 的懒代理虽然解决了初始化阶段的问题,但当你在reactive里塞一个包含几千个对象的数组,并且每个对象都在模板中被渲染时,每个属性的每次读取都会建立依赖。如果列表数据还会频繁更新,trigger触发时的遍历成本也会明显升高。

这个时候可以考虑把不需要响应式的数据排除在响应式系统之外。Vue 3 有shallowReactiveshallowRef,它们只代理对象的第一层,深层改动不会触发更新。还有markRaw,用来标记某个对象永远不会被转为代理。在图表类项目、Canvas 渲染、接入第三方地图 SDK 时,这些 API 非常有用,避免把第三方实例对象整个变成响应式代理导致性能崩掉。

Vue 2 里也有类似的手段:通过Object.freeze冻结不需要响应式的大对象,Vue 的observe函数在看到被冻结对象时,会直接跳过递归处理。这个在分页数据加载、字典数据缓存时特别实用。

注意:shallowReactivemarkRaw不是常规项目的主力方案。只有在确认响应式系统导致性能瓶颈时才用。因为它们一旦引入,会让数据模型的"响应式边界"变得很模糊,团队成员需要额外记住哪些数据是浅响应式、哪些是被标记过的,维护成本反而上升。

5.3 前后端分离项目里的数据流设计

前后端分离的项目中,响应式系统要特别关注数据请求和赋值时机。我见过不少项目里写这样的代码:

const res = await fetchList() this.list = res.data

这段逻辑本身没问题。但如果你在created里先发请求,再在另一个watch里监听某个依赖数据,准备在数据到达后做联动操作,执行顺序就很容易出问题。请求是异步的,而watch默认在值变化时才会触发,如果监听时机在请求返回之前,可能永远不会执行。

比较稳妥的做法是:不在created里直接把分页、筛选、列表状态分散多个响应式字段管理,而是统一用一个"查询条件对象"和"列表状态对象"。查询条件变化时,通过watch自动重新拉取列表,列表数据再进入响应式系统渲染。这里要注意一个 Vue 特有的坑:watch回调里发请求,请求返回后去更新另一个响应式状态。如果用户快速切换多个查询条件,后返回的旧请求可能覆盖新请求的数据。解决办法是给请求加序号或使用AbortController,最后到达的数据才允许赋值。

5.4 组件拆分时的响应式边界

组件拆分时,父子组件之间的数据传递直接影响响应式的追踪范围。父组件通过props传入的数据,在子组件里可以直接读取,因为模板渲染时会访问 props 属性,从而收集依赖。但如果子组件里对 props 做解构,比如const { list } = props,解构出来的是一个普通变量,后续引用的就不再是响应式代理,依赖收集就断了。这在 Vue 3 +<script setup>组合式写法下特别常见。

正确的做法是直接用props.list,或者通过computed包装一个带名字的变量。如果要修改 props 的属性值,规范做法是让子组件抛出事件,父组件在事件回调里修改真正的数据源。这能保持单向数据流,也能让响应式的追踪路径清晰可读。

还有一个常见的边界问题是provide/injectprovide传入一个普通对象,子组件注入后如果直接修改,触发不了父组件更新。正确做法是provide一个reactive对象本身,或者provide一个ref,这样注入方持有的是同一份响应式引用,修改后跨组件触发更新。我们在跨层级共享数据时,首选还是 Pinia 这类状态管理工具,因为provide的方式在大型项目里很容易导致数据流向失控。

5.5 从 "响应式原理" 推导 DevTools 使用技巧

最后聊一个很实用的点:理解了响应式原理,Vue DevTools 就不只是一个看数据的工具了。

在 Vue 3 的 DevTools 里,Components 面板展示的 props、data、computed 等字段都来自响应式系统内部。你可以点击组件查看 "Computed" 的值,配合evaluate功能直接运行表达式,这本质上是手动触发一次 getter,能验证某个computed是否真的重新计算过。如果 computed 的值一直是旧值,说明它的依赖没有被触达,很可能是依赖读取了非响应式的数据。

在 Vue 2 的 DevTools 里,你可以直接看对象属性有没有get/set的紫色标记。没有这个标记的属性一定不在响应式系统内,不管是通过Object.assign合并进来、还是直接在this.$options.data外挂载的属性都不算。这个技巧我每次帮同事排查"弹窗表单赋值后校验不通过"之类的问题都在用,能省掉一半无效排查时间。

6. 结尾:最后分享一点我的实操体会

写 Vue 响应式原理相关代码和排查问题的这几年,我最大的感受是:原理不是用来背的,是用来帮你形成"问题直觉"的。很多人面试前背了 Object.defineProperty 和 Proxy 的区别,但项目里遇到数据不更新,第一反应还是去百度搜索。真正把响应式原理吃透之后,你会下意识先判断"这个修改有没有被拦截层捕获到",而不是盲目试各种 set 用法。

我自己的做法,是在项目里维护一个小型的 "响应式排查看板",把 Vue 2 和 Vue 3 各自需要注意的数据操作方式列成一份清单,每次新同事入职发给他,代码评审时也会按这份清单检查。比如 Vue 2 里凡是出现"新增对象属性、数组下标赋值、直接修改 length"的操作,一律标红要求改成$setsplice;Vue 3 里凡是对reactive对象做解构赋值、展开运算的地方,也会主动 review。花费的时间很少,但"数据改了不更新"这类 issue 在团队里几乎消失了。

这篇更像是我对响应式系统的一次系统性整理,如果你正在准备面试,我建议你不仅要把每个概念点背下来,还要在本地亲手写一遍极简的reactiveeffect实现。哪怕只是几十行,也能把tracktriggeractiveEffect之间的关系刻进脑子里。写完之后再去看 Vue 3 源码,你会突然看懂很多之前只能靠猜测的设计。踩过坑、写过代码、用原理解释过现象之后,这个知识点才真正是你的。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 1:35:22

浙大食品851考研专业课备考资料体系:从初试到复试全攻略

考研这条路&#xff0c;真正拉开差距的往往不是公共课&#xff0c;而是专业课。尤其像浙大食品科学与工程这种专业&#xff0c;851专业课的复习深度和资料质量&#xff0c;直接决定了你是在复试线边缘挣扎&#xff0c;还是稳稳站在录取名单的前排。我见过太多人公共课考得不错&…

作者头像 李华
网站建设 2026/9/10 1:33:43

MATLAB实现计及碳交易与需求响应的微网虚拟电厂日前优化调度

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华