Proxy 与 Object.defineProperty

对比两种数据劫持方式及其在 Vue 响应式中的应用

问题

Proxy 与 Object.defineProperty 有什么区别?为什么 Vue3 选择 Proxy 替代 Object.defineProperty?

解答

Object.defineProperty

Vue2 使用的方案,通过定义属性的 getter/setter 实现劫持。

// 基本用法
const obj = {}
let value = 'hello'

Object.defineProperty(obj, 'msg', {
  get() {
    console.log('读取 msg')
    return value
  },
  set(newVal) {
    console.log('设置 msg:', newVal)
    value = newVal
  }
})

obj.msg        // 读取 msg
obj.msg = 'hi' // 设置 msg: hi
// Vue2 风格的响应式实现
function defineReactive(obj, key, val) {
  Object.defineProperty(obj, key, {
    get() {
      console.log(`get ${key}`)
      return val
    },
    set(newVal) {
      if (newVal === val) return
      console.log(`set ${key}:`, newVal)
      val = newVal
    }
  })
}

// 遍历对象所有属性
function observe(obj) {
  if (typeof obj !== 'object' || obj === null) return
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key])
  })
}

const data = { name: 'Tom', age: 18 }
observe(data)

data.name         // get name
data.age = 20     // set age: 20
data.gender = 'M' // 新增属性,无法监听!

Proxy

Vue3 使用的方案,代理整个对象。

// 基本用法
const obj = { msg: 'hello' }

const proxy = new Proxy(obj, {
  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)
  }
})

proxy.msg        // 读取 msg
proxy.msg = 'hi' // 设置 msg : hi
// Vue3 风格的响应式实现
function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      console.log(`get ${key}`)
      const result = Reflect.get(target, key, receiver)
      // 深层对象递归代理
      if (typeof result === 'object' && result !== null) {
        return reactive(result)
      }
      return result
    },
    set(target, key, value, receiver) {
      console.log(`set ${key}:`, value)
      return Reflect.set(target, key, value, receiver)
    },
    deleteProperty(target, key) {
      console.log(`delete ${key}`)
      return Reflect.deleteProperty(target, key)
    }
  })
}

const data = reactive({ name: 'Tom', age: 18 })

data.name         // get name
data.age = 20     // set age: 20
data.gender = 'M' // set gender: M  ✅ 新增属性也能监听
delete data.age   // delete age     ✅ 删除属性也能监听

数组处理对比

// Object.defineProperty 处理数组
const arr = [1, 2, 3]
arr.forEach((item, index) => {
  Object.defineProperty(arr, index, {
    get() {
      console.log(`get [${index}]`)
      return item
    },
    set(val) {
      console.log(`set [${index}]:`, val)
      item = val
    }
  })
})

arr[0]     // get [0]
arr[0] = 9 // set [0]: 9
arr[3] = 4 // 新增索引,无法监听!
arr.push(5) // 无法监听!Vue2 需要重写数组方法
// Proxy 处理数组
const arr = reactive([1, 2, 3])

arr[0]      // get 0
arr[0] = 9  // set 0: 9
arr[3] = 4  // set 3: 4      ✅ 新增索引能监听
arr.push(5) // get push, get length, set 4: 5, set length: 5  ✅ 原生方法能监听

对比表格

特性Object.definePropertyProxy
监听新增属性❌ 需要 Vue.set✅ 自动监听
监听删除属性❌ 需要 Vue.delete✅ 自动监听
监听数组索引❌ 需要重写方法✅ 自动监听
监听粒度单个属性整个对象
性能初始化时递归遍历惰性代理,访问时才递归
兼容性IE9+不支持 IE

关键点

  • Object.defineProperty 只能劫持已存在的属性,Proxy 代理整个对象
  • Proxy 支持 13 种拦截操作(get、set、deleteProperty、has、ownKeys 等)
  • Proxy 配合 Reflect 使用,保证正确的 this 指向
  • Vue3 的 Proxy 是惰性代理,性能更好(访问时才递归,而非初始化时全量递归)
  • Proxy 无法被 polyfill,这是 Vue3 不支持 IE 的原因