Vue 为什么“改了数据,页面就自动更新”?这件事的本质,就是响应式系统。而 Vue2 和 Vue3 最大的底层变化之一,正是响应式实现方式的升级:Vue2 主要依赖
Object.defineProperty,Vue3 则以Proxy为核心。
如果只用一句话概括两代实现差异:
- Vue2:劫持对象已有属性的
getter / setter,通过Dep + Watcher做依赖收集和派发更新 - Vue3:通过
Proxy代理整个对象,通过track + trigger + effect构建更通用、更完整的响应式系统
一、先理解:什么叫“响应式”?
所谓响应式,本质上就是:
当数据变化时,依赖这个数据的逻辑能够自动重新执行。
例如:
const state = { count: 0 }
function render() { console.log(`count is ${state.count}`)}普通 JavaScript 里,如果你改了:
state.count++render() 不会自动再跑一次。
而 Vue 的目标就是:
- 记录“谁依赖了这个数据”
- 当数据变化时,自动通知这些依赖重新执行
这里面有三个关键问题:
- 怎么知道数据被读取了?
- 怎么知道数据被修改了?
- 怎么找到依赖它的副作用并重新执行?
Vue2 和 Vue3 的实现差异,主要就体现在这三点上。
二、Vue2 的响应式原理:Object.defineProperty
Vue2 的核心思路是:
在初始化阶段,把
data中的每一个属性都转换成带有getter / setter的响应式属性。
Vue2 官方响应式流程图

Vue2 文档里明确提到:当你把一个普通对象传给 Vue 实例的 data 后,Vue 会遍历它的属性,并使用 Object.defineProperty 把这些属性转换成 getter / setter。
2.1 Vue2 的核心流程
大致可以简化成下面几步:
- 初始化时递归遍历
data - 对每个属性通过
Object.defineProperty定义get和set - 在
get里收集依赖 - 在
set里派发更新 - 对应组件的
Watcher重新执行,触发重新渲染
你可以把它理解成一个最简版本:
function defineReactive(obj, key, val) { Object.defineProperty(obj, key, { get() { // 收集依赖 return val }, set(newVal) { val = newVal // 通知依赖更新 } })}三、Vue2 的三个核心角色:Observer、Dep、Watcher
Vue2 的响应式系统里,最关键的三个角色是:
3.1 Observer:把普通对象转成响应式对象
Observer 会递归遍历对象,把每个属性都变成响应式。
也就是说,Vue2 在初始化时会“提前走一遍数据树”。
这也是 Vue2 的一个明显特征:
它更像“初始化时改造数据”,而不是“运行时动态代理对象”。
3.2 Dep:依赖收集器
每个响应式属性内部都会关联一个 Dep,你可以把它理解成:
“这个属性都被谁依赖了”的名单。
当属性被读取时,如果当前有活跃的 Watcher,就把它收集进去。
3.3 Watcher:依赖这个数据的订阅者
Watcher 可以理解成“副作用”或“观察者”:
- 组件渲染 watcher
- 用户自己写的 watcher
- computed 背后的 watcher
当依赖的属性变化时,相关 watcher 会收到通知,然后重新执行。
四、Vue2 中依赖是怎么收集的?
依赖收集发生在 getter 中。
4.1 为什么在 getter 收集?
因为只有当某个属性被“读取”时,Vue 才知道:
“哦,当前这段逻辑依赖了这个属性。”
例如组件渲染函数执行时:
<template> <div>{{ count }}</div></template>渲染模板时会读取 count,这时 count 的 getter 会被触发,于是:
- 发现当前有一个正在执行的渲染 watcher
- 把这个 watcher 收集到
count对应的 Dep 中
这就建立了“数据 -> watcher”的依赖关系。
五、Vue2 中更新是怎么触发的?
更新触发发生在 setter 中。
当你执行:
this.count = this.count + 1会触发 count 的 setter。setter 做的事情大致是:
- 更新内部值
- 调用
dep.notify() - 遍历所有 watcher
- 通知它们重新执行
对于组件来说,最终就是重新走一次渲染流程,生成新的 Virtual DOM,再 patch 到真实 DOM。
六、Vue2 为什么有一些“响应式缺陷”?
这是 Vue2 面试里最经典的一部分,因为它直接暴露了 Object.defineProperty 的局限。
6.1 无法监听对象属性的新增和删除
例如:
vm.obj.newKey = 1这个新属性在初始化时并不存在,因此 Vue2 没有机会为它定义 getter/setter,所以它不是响应式的。
因此 Vue2 里需要:
Vue.set(vm.obj, 'newKey', 1)或者:
this.$set(this.obj, 'newKey', 1)6.2 无法直接监听数组下标修改
例如:
vm.list[1] = 'x'Vue2 不能检测到。
6.3 无法直接监听数组长度修改
例如:
vm.list.length = 2也不是响应式的。
6.4 为什么数组还能“部分响应式”?
因为 Vue2 对数组做了另外一套补丁:
- 重写
push - 重写
pop - 重写
shift - 重写
unshift - 重写
splice - 重写
sort - 重写
reverse
也就是说,Vue2 并不是直接监听数组索引,而是劫持会修改数组的变异方法,在这些方法执行后手动通知更新。
所以在 Vue2 里常见正确写法是:
vm.list.splice(index, 1, newValue)七、Vue2 的异步更新队列
Vue2 不会每次数据变化都立即同步更新 DOM,而是会把更新放进一个队列里,在下一个事件循环 tick 中统一处理。
这样做的好处是:
- 避免重复渲染
- 多次数据变化只触发一次 DOM 更新
- 减少性能浪费
这也是为什么在 Vue2 里经常要配合:
this.$nextTick(() => { // 此时 DOM 已更新})八、Vue3 的响应式原理:Proxy + Reflect
Vue3 最大的变化,是把响应式核心从 Object.defineProperty 换成了 Proxy。
Vue3 响应式原理图

Vue3 官方文档给出的核心伪代码大致是:
function reactive(obj) { return new Proxy(obj, { get(target, key) { track(target, key) return target[key] }, set(target, key, value) { target[key] = value trigger(target, key) } })}这说明 Vue3 的核心思路变成了:
- 使用
Proxy代理整个对象 - 在
gettrap 中收集依赖 - 在
settrap 中触发更新
这相比 Vue2 更自然、更彻底。
九、Vue3 的核心角色:track、trigger、effect
Vue3 不再以 Vue2 那种 Dep + Watcher 的概念暴露给外部,而是内部更偏向下面这几个核心原语:
tracktriggereffectreactiverefcomputed
Vue3 effect / track / trigger 示意图
![]()
9.1 effect:副作用函数
effect 本质上就是:
一段依赖响应式数据的逻辑。
例如组件渲染、computed、watchEffect,本质上都和 effect 有关。
9.2 track:依赖收集
当 effect 执行过程中读取了某个响应式属性,就会触发 track(target, key)。
它做的事情是:
把“当前活跃 effect”收集到
target[key]的依赖集合中。
9.3 trigger:派发更新
当响应式属性被修改时,会触发 trigger(target, key)。
它会找到依赖这个属性的 effect,并让它们重新执行或进入调度队列。
十、Vue3 的依赖结构为什么更通用?
Vue3 官方文档里提到,一个非常关键的数据结构是:
WeakMap<target, Map<key, Set<effect>>>可以这样理解:
WeakMap:按对象维度存依赖Map:对象内部按属性维度存依赖Set:一个属性可能被多个 effect 依赖
也就是:
target -> key -> effects例如:
- 某个响应式对象
user - 它的属性
name - 有两个 effect 依赖了
user.name
那么最终就是:
user -> name -> [effect1, effect2]这套结构的扩展性和表达能力都比 Vue2 更强。
十一、Vue3 为什么能解决 Vue2 的很多限制?
关键就在于 Proxy 是“代理整个对象”,而不是像 Vue2 那样只改造已有属性。
11.1 可以监听属性新增 / 删除
因为 Proxy 可以拦截:
getsetdeletePropertyhasownKeys
所以对象新增属性、删除属性,都能被感知。
11.2 数组支持更完整
Vue3 不需要像 Vue2 那样主要依赖“重写数组变异方法”来兜底。因为数组本质也是对象,索引访问、长度变化都能通过代理感知。
11.3 可以支持 Map / Set
这是 Vue2 基本做不到的能力之一。
Vue3 的响应式系统可以扩展到:
MapSetWeakMapWeakSet
这让响应式系统的通用性大幅提升。
十二、Vue3 的 ref 为什么不是 Proxy?
这是一个非常容易被问到的细节。
Vue3 中:
reactive()主要用于对象,底层依赖Proxyref()主要用于包装单个值,底层依赖带 getter/setter 的.value
官方伪代码大致类似:
function ref(value) { const refObject = { get value() { track(refObject, 'value') return value }, set value(newValue) { value = newValue trigger(refObject, 'value') } } return refObject}所以你可以把它理解成:
- 对象响应式:Proxy
- 单值响应式:getter / setter 包装
十三、Vue2 和 Vue3 响应式实现的本质差异
| 对比项 | Vue2 | Vue3 |
|---|---|---|
| 核心实现 | Object.defineProperty | Proxy |
| 依赖模型 | Dep + Watcher | track + trigger + effect |
| 属性新增删除 | 无法天然监听 | 可以监听 |
| 数组索引 / length | 处理不完整 | 更完整 |
Map / Set 支持 | 基本不支持 | 支持 |
| 初始化成本 | 需要递归遍历数据劫持 | 按访问时代理,能力更灵活 |
| 调试和抽象能力 | 偏组件内部实现 | 可抽象为独立响应式系统 |
十四、Vue3 为什么说响应式系统“更现代”?
这里的“更现代”不只是因为换了 ES6 API,而是因为它的抽象层次更高。
Vue3 的响应式系统已经不只是“服务模板渲染”:
reactiverefcomputedwatchwatchEffect
这些 API 本质上都建立在同一个响应式核心之上。
也就是说,Vue3 把响应式系统从“框架内部黑盒能力”提升成了“可直接编程的底层能力”。
这就是为什么 Vue3 的组合式 API 和逻辑复用能力比 Vue2 强很多。
十五、面试里怎么回答“Vue2 和 Vue3 响应式原理”?
如果在面试里被问到,我建议按这个结构回答:
第一步:先说共同目标
两者本质都是为了实现:
当数据变化时,依赖该数据的视图或副作用自动更新。
第二步:再说 Vue2
- Vue2 会在初始化时递归遍历
data - 通过
Object.defineProperty劫持每个属性的 getter/setter - 在 getter 中依赖收集
- 在 setter 中派发更新
- 通过
Dep和Watcher建立依赖关系
第三步:再说 Vue3
- Vue3 用
Proxy代理整个对象 - 在
get里track - 在
set里trigger - 通过
effect表示副作用 - 底层依赖结构是
WeakMap -> Map -> Set
第四步:最后总结差异
Vue3 相比 Vue2 最大优势在于:
- 能监听新增和删除属性
- 对数组、Map、Set 支持更完整
- 响应式系统更通用,可独立抽象
如果你能按这个层次回答,已经算是比较完整的中高级答案。
十六、总结
Vue2 和 Vue3 的响应式原理,核心并不神秘,本质就是:
- 在数据被读取时记录依赖
- 在数据被修改时通知依赖重新执行
差别在于它们拦截数据访问的方式不同:
- Vue2 用
Object.defineProperty - Vue3 用
Proxy
而这种底层能力的升级,直接带来了 Vue3 在:
- 可维护性
- 可扩展性
- 数据结构支持
- API 抽象能力
上的整体进化。
所以真正精准的总结应该是:
Vue2 是“基于 getter/setter 的组件级响应式系统”,Vue3 则是“基于 Proxy 的通用响应式运行时”。