Vue2与Vue3响应式系统对比

Vue 2 基于 Object.defineProperty 与 Vue 3 基于 Proxy 的响应式系统在拦截粒度、依赖模型、数组处理、新增删除属性、初始化成本等方面的系统对比。

#type / synthesis #status / growing #resource / vue3 #resource / vue2 #resource / javascript

[!info] related notes

Vue2与Vue3响应式系统对比

范围

本篇聚焦 Vue 2 和 Vue 3 在响应式系统实现上的核心差异,包括拦截机制、依赖模型、数组处理、属性增删、初始化策略和可复用性。

为什么要放在一起理解

Vue 2 和 Vue 3 的响应式本质目标一样:

  1. 读取数据时,记录”谁依赖了它”
  2. 修改数据时,通知依赖重新运行

但实现方式从”属性级补丁式劫持”升级成了”对象操作级代理系统”,这带来的不是单点优化,而是整体能力提升。

要把这条线接回 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,代表”需要在数据变化时重新执行的东西”,比如组件渲染函数、computedwatch

当某个 Watcher 正在执行时,Vue 把它挂到全局位置 Dep.target

  1. Watcher 开始执行
  2. 代码里读到 obj.foo
  3. foo 的 getter 触发
  4. getter 发现当前有 Dep.target
  5. 把这个 Watcher 记录到 foo 对应的 dep 里

之后 foo 改变,就能找到所有依赖它的 Watcher,让它们重新执行。

数据劫持流程

data() {
  return {
    user: {
      name: 'Tom'
    }
  }
}

初始化时:

  1. 遍历 user
  2. 遍历 name
  3. name 定义 getter/setter
  4. 如果某个属性值还是对象,继续递归

所以 Vue 2 是”初始化时深度遍历 + 属性劫持”的模式。

数组处理:改写原型方法

defineProperty 很难优雅地拦截 arr[1] = xxxarr.length = 0

所以 Vue 2 对数组用了”改写原型方法”的方式,重写这些能修改数组的方法:

  • push
  • pop
  • shift
  • unshift
  • splice
  • sort
  • reverse

但直接通过下标赋值不行:

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。数据层级深、对象大时,初始化开销明显。

对新数据结构支持差

MapSet 这种,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)
})

执行流程:

  1. effect 运行,读到 state.count
  2. 触发 gettrack(state, 'count') 收集依赖
  3. state.count++ 触发 trigger(state, 'count')
  4. 找到之前依赖 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.getReflect.set

  • 语义更标准Reflect 是与 Proxy 配套设计的官方 API
  • 返回值更合理Reflect.set 返回布尔值,表示设置是否成功
  • 处理原型链和 this 更准确receiver 能保证行为更符合 JS 原生语义

关键差异对比

1. 拦截粒度不同

Vue 2Vue 3
拦截对象的属性拦截整个对象的各种操作
Object.defineProperty(obj, 'key', ...)new Proxy(obj, handler)

2. 初始化时机不同

Vue 2Vue 3
初始化时就递归遍历所有属性并劫持创建代理对象,访问属性时再进入更深层代理,偏惰性

3. 新增/删除属性能力不同

Vue 2Vue 3
默认监听不到,需要 Vue.set / Vue.delete天然支持

4. 数组处理方式不同

Vue 2Vue 3
重写变异方法,索引赋值和 length 修改有问题Proxy 统一代理数组行为

5. 依赖组织方式不同

Vue 2Vue 3
Dep + WatchertargetMap(WeakMap) -> depsMap(Map) -> dep(Set) + effect

6. 可抽离性不同

Vue 2Vue 3
响应式和组件系统绑定更紧响应式系统独立出来,形成通用 API

Vue 3 把响应式系统独立出来了,形成了更通用的 API:

  • reactive
  • ref
  • computed
  • watch
  • effect

所以 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 在触发更新时,通常能知道这是:

  • SET
  • ADD
  • DELETE

这样在处理对象遍历、数组长度、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

reactiveref 底层区别

reactiveref
返回的是对象的 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 2Vue 3
核心实现Object.definePropertyProxy
依赖模型Dep / Watchereffect + WeakMap/Map/Set
对象新增属性默认不能监听可以监听
删除属性默认不能监听可以监听
数组下标修改不友好支持
修改数组 length不友好支持
Map / Set支持差原生支持更好
初始化成本深度遍历较重更惰性
需要 Vue.set需要不需要
独立可复用性较弱很强

面试标准答案

Vue 2 的响应式基于 Object.defineProperty,在初始化时递归遍历 data,通过 getter 做依赖收集、setter 做派发更新,依赖对象通常是 Dep,订阅者是 Watcher。它的缺点是无法监听对象属性新增删除,以及数组下标和 length 的变化,所以需要 Vue.set 和数组变异方法配合。

Vue 3 的响应式基于 ProxyReflect,通过代理整个对象,在 gettrack,在 set/deletetrigger,底层用 WeakMap -> Map -> Set 管理依赖,副作用函数抽象为 effect。相比 Vue 2,它可以天然监听新增删除属性、数组索引、Map/Set 等,响应式能力更完整,设计也更统一。

延伸阅读

创建于 2026/4/2 更新于 2026/5/27