Vue2与Vue3响应式系统对比
Vue 2 基于 Object.defineProperty 与 Vue 3 基于 Proxy 的响应式系统在拦截粒度、依赖模型、数组处理、新增删除属性、初始化成本等方面的系统对比。
[!info] related notes
- 所属 MOC: Vue MOC
- 相关概念: Vue 响应式系统, Vue2 Object.defineProperty 响应式原理, Vue3 Proxy 响应式原理, ECMAScript 代理与反射, vue3, vue-computed, vue-watch-and-watch-effect
- 前置知识: Vue Options API, Vue Composition API
- 实现参考: 手写 Vue reactive 和 watchEffect 的最小实现
- 对照理解: React 状态模型 vs Vue3 响应式模型
- 面试问法: vue-interview-high-frequency
Vue2与Vue3响应式系统对比
范围
本篇聚焦 Vue 2 和 Vue 3 在响应式系统实现上的核心差异,包括拦截机制、依赖模型、数组处理、属性增删、初始化策略和可复用性。
为什么要放在一起理解
Vue 2 和 Vue 3 的响应式本质目标一样:
- 读取数据时,记录”谁依赖了它”
- 修改数据时,通知依赖重新运行
但实现方式从”属性级补丁式劫持”升级成了”对象操作级代理系统”,这带来的不是单点优化,而是整体能力提升。
要把这条线接回 Vue3 的日常使用,可以继续看 vue-reactivity-system.
一句话结论
- Vue 2:先把每个属性改造成 getter/setter(预先劫持)
- Vue 3:直接代理整个对象的一切读写操作(运行时代理)
Vue 2 的响应式原理
核心:Object.defineProperty
Vue 2 在初始化数据时,递归遍历对象的每个属性,把它改造成带 getter/setter 的形式:
function defineReactive(obj, key, val) {
const dep = new Dep()
Object.defineProperty(obj, key, {
get() {
dep.depend()
return val
},
set(newVal) {
if (newVal === val) return
val = newVal
dep.notify()
}
})
}
- 读
obj.key时,触发get,收集依赖 - 写
obj.key = xxx时,触发set,通知更新
依赖收集:Dep + Watcher
Vue 2 里有个经典概念:Watcher,代表”需要在数据变化时重新执行的东西”,比如组件渲染函数、computed、watch。
当某个 Watcher 正在执行时,Vue 把它挂到全局位置 Dep.target:
- Watcher 开始执行
- 代码里读到
obj.foo foo的 getter 触发- getter 发现当前有
Dep.target - 把这个 Watcher 记录到
foo对应的 dep 里
之后 foo 改变,就能找到所有依赖它的 Watcher,让它们重新执行。
数据劫持流程
data() {
return {
user: {
name: 'Tom'
}
}
}
初始化时:
- 遍历
user - 遍历
name - 给
name定义 getter/setter - 如果某个属性值还是对象,继续递归
所以 Vue 2 是”初始化时深度遍历 + 属性劫持”的模式。
数组处理:改写原型方法
defineProperty 很难优雅地拦截 arr[1] = xxx 和 arr.length = 0。
所以 Vue 2 对数组用了”改写原型方法”的方式,重写这些能修改数组的方法:
pushpopshiftunshiftsplicesortreverse
但直接通过下标赋值不行:
arr[0] = 100 // 不能可靠触发更新
arr.length = 0 // 也不行
所以需要这些 API:
Vue.set(arr, 0, 100)
arr.splice(0, 1, 100)
Vue 2 的局限
不能监听对象属性新增/删除
obj.newKey = 123
delete obj.foo
因为 Vue 2 在初始化时只劫持”已经存在的属性”,后面新加的属性没有 getter/setter。
所以需要:
Vue.set(obj, 'newKey', 123)
Vue.delete(obj, 'foo')
不能完整监听数组变化
arr[index] = value不行arr.length = newLen不行
初始化成本高
要递归遍历整个对象,把每个属性都转成 getter/setter。数据层级深、对象大时,初始化开销明显。
对新数据结构支持差
像 Map、Set 这种,Vue 2 的方案不太自然。
Vue 3 的响应式原理
核心:Proxy
Vue 3 用 Proxy 代理整个对象,而不是逐个劫持属性:
const proxy = new Proxy(target, {
get(target, key, receiver) {
track(target, key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
trigger(target, key)
return result
}
})
这意味着:
- 访问哪个 key,运行时再拦截
- 设置哪个 key,运行时也能知道
- 新增属性、删除属性,也都能感知
依赖收集结构:WeakMap => Map => Set
Vue 3 没有沿用 Vue 2 的 Dep + Watcher,而是更通用的数据结构:
targetMap = WeakMap {
target1 => Map {
'name' => Set(effect1, effect2),
'age' => Set(effect3)
},
target2 => Map {
'count' => Set(effect4)
}
}
- 第一层:某个被代理对象
target - 第二层:这个对象的某个属性
key - 第三层:依赖这个属性的副作用函数
effect
核心概念:effect
Vue 3 中更核心的是 effect,代表”依赖响应式数据的函数”:
effect(() => {
console.log(state.count)
})
执行流程:
effect运行,读到state.count- 触发
get,track(state, 'count')收集依赖 state.count++触发trigger(state, 'count')- 找到之前依赖
count的 effect,重新执行
track / trigger 简化实现
track
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)
}
dep.add(activeEffect)
}
trigger
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
effects && effects.forEach(effect => effect())
}
这套模型比 Vue 2 更统一,也更容易扩展。
为什么配合 Reflect
Vue 3 在 Proxy 里通常不用 target[key] 和 target[key] = value,而用 Reflect.get 和 Reflect.set:
- 语义更标准:
Reflect是与Proxy配套设计的官方 API - 返回值更合理:
Reflect.set返回布尔值,表示设置是否成功 - 处理原型链和 this 更准确:
receiver能保证行为更符合 JS 原生语义
关键差异对比
1. 拦截粒度不同
| Vue 2 | Vue 3 |
|---|---|
| 拦截对象的属性 | 拦截整个对象的各种操作 |
Object.defineProperty(obj, 'key', ...) | new Proxy(obj, handler) |
2. 初始化时机不同
| Vue 2 | Vue 3 |
|---|---|
| 初始化时就递归遍历所有属性并劫持 | 创建代理对象,访问属性时再进入更深层代理,偏惰性 |
3. 新增/删除属性能力不同
| Vue 2 | Vue 3 |
|---|---|
默认监听不到,需要 Vue.set / Vue.delete | 天然支持 |
4. 数组处理方式不同
| Vue 2 | Vue 3 |
|---|---|
| 重写变异方法,索引赋值和 length 修改有问题 | Proxy 统一代理数组行为 |
5. 依赖组织方式不同
| Vue 2 | Vue 3 |
|---|---|
Dep + Watcher | targetMap(WeakMap) -> depsMap(Map) -> dep(Set) + effect |
6. 可抽离性不同
| Vue 2 | Vue 3 |
|---|---|
| 响应式和组件系统绑定更紧 | 响应式系统独立出来,形成通用 API |
Vue 3 把响应式系统独立出来了,形成了更通用的 API:
reactiverefcomputedwatcheffect
所以 Vue 3 的响应式能力不仅能服务组件,也能在组件外独立使用。
Vue 3 比 Vue 2 强在哪
能监听新增和删除属性
state.newKey = 1
delete state.foo
都能触发响应式,因为 Proxy 拦截的是整个对象的操作,不依赖初始化时预先定义属性。
这也是 Vue 3 不再需要 Vue.set / Vue.delete 的重要原因。
数组支持更自然
arr[0] = 100
arr.length = 0
arr.push(1)
相比 Vue 2,数组这一块自然得多。
支持 Map / Set / WeakMap / WeakSet
const m = reactive(new Map())
m.set('a', 1)
这在 Vue 2 的体系里很难优雅实现。
惰性代理,通常更省
Vue 2 是”先递归劫持整个对象”。 Vue 3 更像”访问到哪里,代理和依赖就处理到哪里”。
所以在很多场景下:
- 初始化更轻
- 大对象场景更合理
- 不用一次性深度 walk 全量数据
可以区分操作类型
Vue 3 在触发更新时,通常能知道这是:
SETADDDELETE
这样在处理对象遍历、数组长度、Map/Set 迭代时更精细。
更容易做调度优化
effect 通常可以配合 scheduler,不一定每次 trigger 都立刻执行。这让批量更新、异步刷新、去重调度更自然。
更容易形成独立响应式库
Vue 3 的响应式模块已经不是”只能给 Vue 组件用的黑盒”,而是一套相对通用的响应式运行时。
为什么 Vue 3 里还有 ref
很多人会问:既然有 Proxy,为什么不全都用 reactive,还要有 ref?
因为 Proxy 只能代理对象,不能直接代理原始值。
let count = 0
你没法对 0 直接做 Proxy。
所以 Vue 3 提供了 ref,把原始值包装成对象:
const count = ref(0)
本质上类似:
{
value: 0
}
再对这个对象的 value 做响应式处理。
所以:
- 对象类型 常用
reactive - 基本类型 常用
ref
reactive 和 ref 底层区别
| reactive | ref |
|---|---|
| 返回的是对象的 Proxy | 返回的是一个带 .value 的包装对象 |
| 深层对象也会被响应式代理 | 读取 .value 时 track,修改 .value 时 trigger |
| 适合对象、数组、Map、Set | 适合基本类型 |
ref 的伪代码类似:
class RefImpl {
get value() {
track(this, 'value')
return this._value
}
set value(newVal) {
this._value = newVal
trigger(this, 'value')
}
}
简化伪代码对比
Vue 2
function observe(data) {
if (!isObject(data)) return
Object.keys(data).forEach(key => {
defineReactive(data, key, data[key])
observe(data[key])
})
}
特点:
- 先递归
- 再逐属性 defineProperty
Vue 3
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
track(target, key)
const res = Reflect.get(target, key, receiver)
if (isObject(res)) {
return reactive(res)
}
return res
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
trigger(target, key)
return result
}
})
}
特点:
- 代理整个对象
- 访问时再深入处理
实际开发中最直观的差异
Vue 2 里经常要绕路
this.$set(this.obj, 'a', 1)
this.arr.splice(index, 1, newValue)
Vue 3 里通常直接写就行
state.obj.a = 1
state.arr[index] = newValue
这背后其实就是响应式系统能力提升。
完整对比表
| 对比项 | Vue 2 | Vue 3 |
|---|---|---|
| 核心实现 | Object.defineProperty | Proxy |
| 依赖模型 | Dep / Watcher | effect + WeakMap/Map/Set |
| 对象新增属性 | 默认不能监听 | 可以监听 |
| 删除属性 | 默认不能监听 | 可以监听 |
| 数组下标修改 | 不友好 | 支持 |
| 修改数组 length | 不友好 | 支持 |
| Map / Set | 支持差 | 原生支持更好 |
| 初始化成本 | 深度遍历较重 | 更惰性 |
需要 Vue.set | 需要 | 不需要 |
| 独立可复用性 | 较弱 | 很强 |
面试标准答案
Vue 2 的响应式基于
Object.defineProperty,在初始化时递归遍历 data,通过 getter 做依赖收集、setter 做派发更新,依赖对象通常是Dep,订阅者是Watcher。它的缺点是无法监听对象属性新增删除,以及数组下标和 length 的变化,所以需要Vue.set和数组变异方法配合。Vue 3 的响应式基于
Proxy和Reflect,通过代理整个对象,在get时track,在set/delete时trigger,底层用WeakMap -> Map -> Set管理依赖,副作用函数抽象为effect。相比 Vue 2,它可以天然监听新增删除属性、数组索引、Map/Set 等,响应式能力更完整,设计也更统一。
延伸阅读
- Vue 3 响应式 API 详解:Vue 响应式系统
- Proxy 与 Reflect 详解:ECMAScript 代理与反射
- 手写最小实现:手写 Vue reactive 和 watchEffect 的最小实现
- Vue 3 定位与学习主线:vue3
- React vs Vue 3 响应式对比:React 状态模型 vs Vue3 响应式模型