返回专题首页

Vue 专题

Vue2 响应式原理:Object.defineProperty、依赖收集与更新限制

Vue2 响应式原理之所以值得认真学,不是因为今天新项目还会大量用 Vue2,而是因为很多真实业务仍然建立在它之上。更重要的是,只有理解 Vue2 为什么会有那些经典坑,你才真正能看懂 Vue3 为什么要换一套响应式底层。

Vue 专题第 07 篇 / 26 篇6 分钟

Vue2 响应式原理之所以值得认真学,不是因为今天新项目还会大量用 Vue2,而是因为很多真实业务仍然建立在它之上。更重要的是,只有理解 Vue2 为什么会有那些经典坑,你才真正能看懂 Vue3 为什么要换一套响应式底层。

这一篇重点要讲清四件事:

  • Vue2 怎样把普通对象变成“可观察数据”;
  • 依赖收集和更新派发到底在做什么;
  • 为什么它会有对象新增属性、数组索引更新这些经典限制;
  • 把这些原理放回项目排错和迁移判断里,有什么现实价值。

Vue2 响应式系统最核心的思路是什么?

Vue2 的关键做法,是在初始化阶段遍历数据对象,然后用 Object.defineProperty 为已有属性定义 getter 和 setter。这样一来:

  • 读取属性时会进入 getter;
  • 修改属性时会进入 setter;
  • getter 可以顺手收集“谁依赖了我”;
  • setter 可以通知这些依赖“我变了,你们该更新了”。

所以 Vue2 不是“神奇地知道数据变化”,而是通过属性级劫持,让数据读写被框架接管。

依赖收集到底在收什么?

很多人会记住“Vue2 有依赖收集”,但如果不把它具体化,这句话其实没什么帮助。更贴近项目的理解是:

  • 组件渲染会读取模板中用到的数据;
  • 读取数据时,getter 会记录当前活跃的渲染上下文;
  • 这个渲染上下文通常对应某个 watcher;
  • 以后数据变化时,setter 就能通知这些 watcher 重新工作。

也就是说,依赖收集真正回答的是:

  • 哪些状态被当前组件或计算逻辑用到了?
  • 当这些状态变化时,应该通知谁?

一旦理解成这条链路,你就会知道 computedwatch、组件重渲染虽然用途不同,但都和这套依赖记录机制有关。

watcher 在 Vue2 里为什么这么关键?

可以把 watcher 理解成“响应式系统里的订阅执行单元”。它负责承接不同种类的响应更新,比如:

  • 组件渲染更新;
  • 计算属性重新求值;
  • watch 监听回调执行。

这也是为什么很多 Vue2 问题最后都能回到 watcher 体系去理解。因为从本质上看,响应式系统不是在“自动更新页面”,而是在“数据变化 -> 通知订阅者 -> 订阅者决定怎么更新”。

为什么 Vue2 会有对象新增属性不响应的问题?

这是最经典的 Vue2 限制之一,根因其实非常直接:因为只有初始化阶段已经存在的属性,才被 Object.defineProperty 包装过。

如果你后面运行时再给对象加一个新字段,这个字段并没有 getter/setter,自然也就不会被响应式系统感知。于是才会出现:

  • 新增字段后视图不更新;
  • 需要借助 Vue.set / this.$set
  • 表单模型如果字段事先没声明完整,后面容易出问题。

这说明 Vue2 响应式的限制,并不是“框架偶尔抽风”,而是和它的实现方式直接绑定的。

数组为什么也是 Vue2 的高频坑点?

数组的问题比对象更容易让人困惑。因为数组并不是普通对象字段访问那套逻辑,很多操作发生在索引和长度变化层。Vue2 没法像 Proxy 那样天然拦住每一个数组索引变化,于是只能通过重写部分变异方法来间接追踪更新。

这就导致几个典型问题:

  • 直接改数组下标,响应式行为不稳定;
  • 直接改 length 不是可靠更新方式;
  • 更推荐通过 splice 等变异方法更新。

从项目视角看,重要的不是死记哪些方法能触发,而是知道 Vue2 对数组的追踪本来就不是完全自然的。

这些限制会怎样影响真实项目?

一旦进入中后台、动态表单、复杂列表、权限树、配置驱动页面,Vue2 响应式限制就会很容易暴露出来。典型表现包括:

  • 动态表单里新增字段后界面不更新;
  • 给表格行对象临时挂状态字段却没反应;
  • 列表项直接按索引改值,结果局部 UI 不刷新;
  • 复杂嵌套对象更新后,需要强制走替换式写法。

所以理解 Vue2 原理最大的实际价值,不是为了讲原理,而是让你看到这些 bug 时能第一时间判断根因,而不是靠重刷页面或强制更新硬顶。

为什么说 Vue2 项目里更要提前设计数据结构?

因为它对“运行时临时长字段”的容忍度比较低。一个更稳的习惯通常是:

  • 在初始化时尽量声明完整数据结构;
  • 对动态字段有明确建模,而不是临时挂载;
  • 数组更新优先使用更稳定的方式;
  • 避免在深层对象上做过于随意的结构修改。

这听上去像工程习惯,但本质上是响应式实现能力边界倒逼出来的设计要求。

Vue2 原理为什么对迁移 Vue3 很有帮助?

因为只要你真正理解了 Vue2 的这些限制,就会看懂 Vue3 用 Proxy 之后,底层能力为什么明显增强:

  • 对对象和数组的拦截更自然;
  • 对新增属性和索引变化的处理更统一;
  • 响应式表达和组合式 API 更容易形成闭环。

这样你面对 Vue2 -> Vue3 迁移时,就不会只把它看成“语法重写”,而会意识到很多旧项目里的绕路写法,原本就是为了绕开 Vue2 响应式边界。

最常见的几个误区

1. 把 Vue2 响应式理解成“自动双向绑定”

这个说法太粗,会让你忽略真正关键的 getter / setter 和依赖收集逻辑。

2. 数据不更新就条件反射强刷组件

如果不先判断是不是新增属性或数组索引问题,很容易治标不治本。

3. 不理解数组和对象的限制,却在复杂场景里随意改结构

这会持续制造难排查问题。

4. 只把 Vue2 和 Vue3 差异理解成 API 写法不同

其实很多差异的根源在响应式底层。

总结

Vue2 响应式系统的本质,是通过 Object.defineProperty、watcher 和依赖收集,把数据读取与视图更新连接起来。也正因为它建立在属性级劫持之上,对象新增属性和数组索引更新才会成为经典限制。只要你把“劫持方式、依赖收集、更新派发、实现边界”这四件事真正想清楚,Vue2 项目里的很多奇怪问题都会变得更容易解释,Vue3 的演进方向也会更容易理解。