Skip to content

Vue2 vs Vue3 响应式原理深入对比(Object.defineProperty vs Proxy)

一、核心要点速览

💡 核心考点

  • Vue2: 基于 Object.defineProperty 的数据劫持
  • Vue3: 基于 Proxy 的代理机制
  • 关键差异: 性能、数组支持、对象新增属性检测

二、Vue2 响应式原理

1. Object.defineProperty 实现机制

Vue2 使用 Object.defineProperty 劫持对象的 getter 和 setter:

javascript
// Vue2 响应式简化实现
function defineReactive(obj, key, value) {
  const dep = new Dep()
  
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      // 收集依赖
      Dep.target && dep.addSub(Dep.target)
      return value
    },
    set(newVal) {
      if (newVal !== value) {
        value = newVal
        // 通知更新
        dep.notify()
      }
    }
  })
}

2. 响应式系统架构图

┌──────────────────────────────────────────────────────────┐
│                    Vue2 响应式系统                        │
│                                                          │
│  数据对象 (Data)                                          │
│  ┌────────────────────────────────────┐                 │
│  │  data: {                           │                 │
│  │    name: 'Vue',                    │                 │
│  │    age: 3                          │                 │
│  │  }                                 │                 │
│  └─────────────┬──────────────────────┘                 │
│                │ 遍历转换                                │
│                ▼                                         │
│  ┌─────────────────────────────────────────┐            │
│  │  Object.defineProperty 劫持             │            │
│  │  ┌──────────┐  ┌──────────┐            │            │
│  │  │  name    │  │   age    │            │            │
│  │  │ getter   │  │  getter  │            │            │
│  │  │  setter  │  │  setter  │            │            │
│  │  └────┬─────┘  └────┬─────┘            │            │
│  └───────┼─────────────┼──────────────────┘            │
│          │             │                                │
│          ▼             ▼                                │
│  ┌────────────┐ ┌────────────┐                         │
│  │  Dep 1     │ │  Dep 2     │ ← 依赖收集器             │
│  │ (name 的)  │ │ (age 的)   │                         │
│  └─────┬──────┘ └─────┬──────┘                         │
│        │              │                                 │
│        ▼              ▼                                 │
│  ┌──────────┐   ┌──────────┐                           │
│  │ Watcher  │   │ Watcher  │ ← 观察者                   │
│  │ (组件渲染)│   │ (计算属性)│                          │
│  └──────────┘   └──────────┘                           │
└──────────────────────────────────────────────────────────┘

工作流程:
1. 初始化时遍历 data 的所有属性
2. 使用 Object.defineProperty 转换为 getter/setter
3. 组件渲染时触发 getter,收集 Watcher 依赖
4. 数据变化时触发 setter,通知 Watcher 更新

3. 依赖收集与时序图

时间 →  ─────────────────────────────────────────────────►

组件挂载阶段:
┌─────────┐
│ 组件渲染 │
└────┬────┘
     │ 读取 data.name

┌─────────────────┐
│ Object.get(name)│ ← 触发 getter
└────────┬────────┘
         │ 收集依赖

┌─────────────────┐
│ Dep.addSub(     │
│   currentWatcher│
│ )               │
└─────────────────┘

数据更新阶段:
┌─────────┐
│data.name =│
│ 'New Vue'│
└────┬────┘
     │ 设置新值

┌─────────────────┐
│ Object.set(name)│ ← 触发 setter
└────────┬────────┘
         │ 通知更新

┌─────────────────┐
│ Dep.notify()    │
└────────┬────────┘


┌─────────────────┐
│ Watcher.update()│
└────────┬────────┘


┌─────────────────┐
│ 重新渲染组件     │
└─────────────────┘

三、Vue2 的局限性

1. 无法检测对象属性的添加或删除

javascript
const obj = { name: 'Vue' }

// ❌ 这样操作不会触发视图更新
obj.age = 3

// ✓ 必须使用 Vue.set
Vue.set(obj, 'age', 3)

// ✓ 或使用 this.$set
this.$set(this.obj, 'age', 3)

原因分析:

Vue2 初始化时的处理:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
初始化:
  data = { name: 'Vue' }
  
  Object.defineProperty(data, 'name', {
    get() { /* 收集依赖 */ },
    set() { /* 通知更新 */ }
  })
  
问题:
  只劫持了已存在的 'name' 属性
  
  执行 obj.age = 3 时:
  ┌────────────────────────────────┐
  │  obj = { name: 'Vue', age: 3 } │
  │       ↑                        │
  │       └─ age 没有 getter/setter│
  │                                  │
  │  ❌ Vue 无法检测到 age 的变化   │
  └────────────────────────────────┘
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

2. 数组的响应式限制

Vue2 对数组的特殊处理:

javascript
const arr = [1, 2, 3]

// ✓ 这些方法会触发更新(重写了数组原型)
arr.push(4)
arr.pop()
arr.shift()
arr.unshift(5)
arr.splice(1, 1, 'new')

// ❌ 这些操作不会触发更新
arr[0] = 100        // 通过索引设置
arr.length = 5      // 修改长度

解决方案:

javascript
// ✓ 使用 splice 代替索引设置
arr.splice(0, 1, 100)

// ✓ 使用 splice 代替 length 修改
arr.splice(5)  // 截断到长度 5

3. 性能问题

深度遍历的性能开销:

假设有一个深层嵌套的对象:
data = {
  user: {
    profile: {
      name: 'Vue',
      age: 3,
      skills: ['JS', 'CSS', 'HTML']
    }
  }
}

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
初始化成本:
  ┌───────────────────────────────────────┐
  │ 递归遍历所有层级                        │
  │                                       │
  │ data.user         → defineProperty   │
  │   └─ profile      → defineProperty   │
  │       ├─ name     → defineProperty   │
  │       ├─ age      → defineProperty   │
  │       └─ skills   → defineProperty   │
  │           ├─ [0]  → defineProperty   │
  │           ├─ [1]  → defineProperty   │
  │           └─ [2]  → defineProperty   │
  │                                       │
  │ 总计:7 次 Object.defineProperty 调用  │
  └───────────────────────────────────────┘

问题:
  ❌ 即使某些属性从未使用,也会被转换为响应式
  ❌ 深层嵌套对象导致大量递归调用
  ❌ 初始化性能随嵌套深度指数下降
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

四、Vue3 的革新 - Proxy

1. Proxy 基础语法

// ES6 Proxy 基本用法
const target = { name: 'Vue' }

const handler = {
  get(target, key, receiver) {
    console.log(`读取 ${key}`)
    return Reflect.get(target, key, receiver)
  },
  set(target, key, value, receiver) {
    console.log(`设置 ${key} = ${value}`)
    return Reflect.set(target, key, value, receiver)
  }
}

const proxy = new Proxy(target, handler)

proxy.name        // 读取 name
proxy.name = 'Vue3'  // 设置 name = Vue3
proxy.age = 3     // 设置 age = 3 (自动拦截!)

2. Vue3 响应式实现

// Vue3 reactive 简化实现
function createReactiveObject(target) {
  const handler = {
    get(target, key, receiver) {
      // 收集依赖
      track(target, key)
      
      // 懒代理:访问时才递归创建 proxy
      const res = Reflect.get(target, key, receiver)
      return isObject(res) ? reactive(res) : 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) {
      // 拦截删除属性
      trigger(target, key)
      return Reflect.deleteProperty(target, key)
    }
  }
  
  return new Proxy(target, handler)
}

3. Vue3 响应式架构对比

┌──────────────────────────────────────────────────────────┐
│                    Vue3 响应式系统                        │
│                                                          │
│  数据对象 (Data)                                          │
│  ┌────────────────────────────────────┐                 │
│  │  data: {                           │                 │
│  │    name: 'Vue3',                   │                 │
│  │    age: 3                          │                 │
│  │  }                                 │                 │
│  └─────────────┬──────────────────────┘                 │
│                │ 创建代理                                │
│                ▼                                         │
│  ┌─────────────────────────────────────────┐            │
│  │         Proxy 代理层                     │            │
│  │  ┌─────────────────────────────┐       │            │
│  │  │      Proxy Handler          │       │            │
│  │  │  ┌─────┬─────┬──────────┐  │       │            │
│  │  │  │ get │ set │ delete...│  │       │            │
│  │  │  └──┬──┴──┬──┴────┬─────┘  │       │            │
│  │  │     │     │       │        │       │            │
│  │  └─────┼─────┼───────┼────────┘       │            │
│  └────────┼─────┼───────┼────────────────┘            │
│           │     │       │                              │
│           ▼     ▼       ▼                              │
│  ┌────────────────────────────┐                       │
│  │    统一依赖管理 (Dep)       │ ← 只有一个 Dep         │
│  └─────────────┬──────────────┘                       │
│                │                                       │
│                ▼                                       │
│  ┌────────────────────────────┐                       │
│  │    Effect / Watcher        │ ← 响应式效应           │
│  └────────────────────────────┘                       │
└──────────────────────────────────────────────────────────┘

核心优势:
✓ 无需遍历所有属性,按需代理
✓ 自动拦截新增/删除属性
✓ 完整的数组支持
✓ 更少的递归调用

五、Vue2 vs Vue3 详细对比

1. 对象新增属性检测

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
场景:给对象添加新属性

Vue2 (Object.defineProperty):
┌─────────────────────────────────────────┐
│  const obj = { name: 'Vue' }            │
│                                         │
│  初始化时:                             │
│  ┌─────────────────────────────┐       │
│  │ 只劫持 'name' 属性           │       │
│  │ Object.defineProperty(obj,  │       │
│  │   'name', {...})            │       │
│  └─────────────────────────────┘       │
│                                         │
│  执行 obj.age = 3:                      │
│  ┌─────────────────────────────┐       │
│  │ 直接添加属性                │       │
│  │ ❌ 不触发任何 getter/setter │       │
│  │ ❌ Vue 无法检测             │       │
│  └─────────────────────────────┘       │
└─────────────────────────────────────────┘

Vue3 (Proxy):
┌─────────────────────────────────────────┐
│  const obj = { name: 'Vue' }            │
│                                         │
│  初始化时:                             │
│  ┌─────────────────────────────┐       │
│  │ 创建整个对象的 Proxy         │       │
│  │ const proxy = new Proxy(    │       │
│  │   obj, handler)             │       │
│  └─────────────────────────────┘       │
│                                         │
│  执行 proxy.age = 3:                    │
│  ┌─────────────────────────────┐       │
│  │ 触发 handler.set()          │       │
│  │ ✓ 成功拦截                  │       │
│  │ ✓ 收集依赖并触发更新        │       │
│  └─────────────────────────────┘       │
└─────────────────────────────────────────┘
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

2. 数组响应式支持

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
场景:通过索引修改数组元素

Vue2:
arr = [1, 2, 3]

执行 arr[0] = 100:
┌──────────────────────────────────────────┐
│  arr[0] = 100                            │
│     │                                    │
│     ❌ 不触发响应式更新                   │
│                                          │
│  原因:                                  │
│  - Vue2 重写了数组原型方法               │
│  - 但无法拦截索引赋值操作                │
│                                          │
│  解决:arr.splice(0, 1, 100)            │
└──────────────────────────────────────────┘

Vue3:
arr = [1, 2, 3]

执行 arr[0] = 100:
┌──────────────────────────────────────────┐
│  arr[0] = 100                            │
│     │                                    │
│     ✓ 触发 Proxy.set('0', 100)          │
│     ✓ 正常触发响应式更新                 │
│                                          │
│  优势:                                  │
│  - Proxy 原生支持索引操作                │
│  - 无需特殊处理数组方法                  │
└──────────────────────────────────────────┘
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

3. 性能对比

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
初始化性能对比(深层嵌套对象)

data = {
  a: { b: { c: { d: { e: { f: 'deep' } } } } } }
}

Vue2 (Object.defineProperty):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
遍历路径:
  data.a → defineProperty
    └─ b → defineProperty
      └─ c → defineProperty
        └─ d → defineProperty
          └─ e → defineProperty
            └─ f → defineProperty

总调用次数:6 次 Object.defineProperty
时间复杂度:O(n),n = 所有属性数量

问题:
  ❌ 必须遍历所有层级
  ❌ 即使只用到 data.a.b.c
  ❌ 深层嵌套性能急剧下降
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Vue3 (Proxy):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
创建代理:
  data → 创建 1 个 Proxy
    └─ 访问 data.a 时才创建 a 的 Proxy
      └─ 访问 data.a.b 时才创建 b 的 Proxy
        └─ ...

初始调用次数:仅 1 次 Proxy 创建
时间复杂度:O(1) 初始,后续按需 O(1)

优势:
  ✓ 懒加载,按需代理
  ✓ 未访问的属性不创建代理
  ✓ 深层嵌套性能优势明显
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

性能提升:
  浅层对象(< 5 层):~2 倍
  中等嵌套(5-10 层):~5 倍
  深层嵌套(> 10 层):~10 倍以上

4. 完整对比表格

特性Vue2 (Object.defineProperty)Vue3 (Proxy)
检测新增属性❌ 不支持(需 Vue.set)✓ 支持
检测删除属性❌ 不支持(需 Vue.delete)✓ 支持
数组索引修改❌ 不支持✓ 支持
数组 length 修改❌ 不支持✓ 支持
初始化方式递归遍历所有属性懒代理,按需创建
性能开销高(O(n))低(O(1))
Map/Set 支持❌ 需要特殊处理✓ 原生支持
代码量~300 行~150 行

六、面试标准回答

Vue2 的响应式基于 Object.defineProperty。它在初始化时会递归遍历对象的所有属性,将其转换为 getter 和 setter。当组件渲染时读取数据会触发 getter 进行依赖收集,数据变化时触发 setter 通知 Watcher 更新。

但 Vue2 存在三个主要局限

  1. 无法检测对象属性的添加或删除,需要使用 Vue.set/Vue.delete
  2. 数组的索引赋值和 length 修改不会触发更新,需要重写数组原型方法
  3. 初始化时需要遍历所有属性,深层嵌套对象性能较差

Vue3 改用 ES6 Proxy 重构了响应式系统。Proxy 可以直接代理整个对象,通过 handler 拦截 get、set、deleteProperty 等操作。它采用懒加载策略,只有在访问嵌套对象时才创建新的 Proxy。

Proxy 的优势包括

  1. 完整支持对象属性的增删改查
  2. 原生支持数组索引操作
  3. 支持 Map、Set 等数据结构
  4. 初始性能更好,无需遍历所有属性

性能方面,Vue3 在浅层对象上约有 2 倍提升,深层嵌套场景可达 10 倍以上。


七、延伸思考

1. Proxy 的兼容性处理

// Vue3 的兼容性降级策略
if (typeof Proxy !== 'undefined') {
  // 使用 Proxy 实现
  createReactiveObject = (target) => new Proxy(target, handler)
} else {
  // 降级到 Object.defineProperty (如 IE11)
  // 实际上 Vue3 不支持 IE11
  console.warn('Proxy not supported, Vue3 cannot run')
}

2. ref 与 reactive 的选择

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
使用场景决策树:

需要响应式数据?

    ├─ 原始类型 (string, number, boolean)
    │   └─ 使用 ref()
    │       └─ 通过 .value 访问

    └─ 引用类型 (object, array)

        ├─ 需要替换整个对象?
        │   ├─ 是 → 使用 ref()
        │   └─ 否 → 使用 reactive()

        └─ 需要保持引用稳定性?
            ├─ 是 → 使用 reactive()
            └─ 否 → 使用 ref()
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

3. 性能优化建议

// ✓ 好的实践:标记不需要响应式的对象
const largeData = shallowRef({ /* 大数据 */ })

// ✓ 好的实践:使用 readonly 避免不必要的更新
const config = readonly({ api: '/api' })

// ❌ 避免:过度深层的 reactive
const state = reactive({
  a: { b: { c: { d: { e: { /* ... */ } } } } }
})

// ✓ 更好的:扁平化状态
const state = reactive({
  a_b_c_d_e: value
})

八、记忆口诀

响应式原理歌诀:

Vue2 用 defineProperty,
getter setter 来劫持。
新增属性检测不到,
数组索引也无力。

Vue3 升级用 Proxy,
整个代理更给力。
增删改查全支持,
懒加载省性能。

面试答题要记牢:
原理 + 局限 + 对比,
性能提升摆数据,
代码示例不能少!

九、推荐资源


十、总结一句话

  • Vue2: Object.defineProperty + 递归遍历 + 数组原型重写 = 有缺陷的响应式 ⚠️
  • Vue3: Proxy + 懒加载 + 完整拦截 = 完美的响应式
最近更新