Vue技术——JavaScript 数据代理的实现与应用

Vue 中的“数据代理”(Data Proxy)实现与应用详解

在 Vue(尤其是 Vue 2)中,数据代理数据劫持/响应式 是两个密切相关但又不同的概念。很多人把它们混在一起,今天我们彻底拆开来讲清楚。

1. 什么是“数据代理”?(Vue 2 & Vue 3 通用概念)

数据代理:让 vm(Vue 实例)直接代理访问 data 对象里的属性。

写法对比:

// Vue 2 / Vue 3 都支持这种写法
const vm = new Vue({
  data: {
    msg: "hello",
    user: { name: "重阳" }
  }
})

// 不用写 vm.$data.msg
console.log(vm.msg)          // hello          ← 代理后直接访问
vm.msg = "hi"                // 也能直接修改

没有数据代理时的写法(原始):

console.log(vm.$data.msg)    // 麻烦
vm.$data.msg = "hi"

核心目的:让开发者书写更简洁、自然,像操作普通对象一样操作 Vue 实例。

2. Vue 2 中的数据代理实现(Object.defineProperty + 循环)

Vue 2 在 _initData 阶段做了两件事:

  1. 数据劫持(响应式):用 Object.definePropertydata 每个属性改造成带 getter/setter 的
  2. 数据代理:把 data 的每个 key 代理到 vm 实例上

简化核心代码(伪代码风格):

function initData(vm) {
  const data = vm.$options.data.call(vm)  // 执行 data 函数得到对象
  vm._data = data                         // 内部保存一份

  // 第一步:数据代理(让 vm.msg → vm._data.msg)
  for (let key in data) {
    proxy(vm, '_data', key)
  }

  // 第二步:响应式劫持(observe)
  observe(data)
}

// 代理函数(最核心的三行)
function proxy(target, sourceKey, key) {
  Object.defineProperty(target, key, {
    enumerable: true,
    configurable: true,
    get() {
      return this[sourceKey][key]   // vm.msg → vm._data.msg
    },
    set(newVal) {
      this[sourceKey][key] = newVal // vm.msg = xx → vm._data.msg = xx
    }
  })
}

结论:Vue 2 的数据代理本质是用 Object.defineProperty 在 Vue 实例上为每个 data 属性定义了 getter/setter,只是不做依赖收集,只做转发。

3. Vue 3 中的数据代理(Proxy 时代的变化)

Vue 3 完全抛弃了 Object.defineProperty 的属性级劫持,改用 Proxy 代理整个对象

但“数据代理”这个概念依然存在,只是实现方式变了。

Vue 3 源码中(简化):

// reactive 函数返回的就是代理对象
export function reactive<T extends object>(target: T): T {
  return createReactiveObject(target, ...)
}

const reactiveMap = new WeakMap()

function createReactiveObject(target) {
  // ... 省略边界判断

  const proxy = new Proxy(target, {
    get(target, key, receiver) {
      // 1. 依赖收集 track()
      track(target, key)

      const res = Reflect.get(target, key, receiver)

      // 如果是对象,继续代理(深度响应式)
      if (isObject(res)) {
        return reactive(res)   // 惰性代理
      }
      return res
    },

    set(target, key, value, receiver) {
      const oldValue = Reflect.get(target, key, receiver)
      const result = Reflect.set(target, key, value, receiver)

      // 2. 触发更新 trigger()
      if (oldValue !== value) {
        trigger(target, key)
      }
      return result
    },

    // deleteProperty、has、ownKeys 等也都有拦截
  })

  reactiveMap.set(target, proxy)
  return proxy
}

Vue 3 里“数据代理”体现在哪里?

setup / ref/reactive 返回的对象,最终挂载到组件实例 proxyRefs / setupState 上时,也做了类似代理,让 this.msg 可以直接访问 setup 返回的属性。

但更常见的是:开发者直接使用 reactive 返回的 proxy 对象,不再需要额外的“代理到 vm”这一步。

4. Vue 2 vs Vue 3 数据代理 & 响应式对比(面试必背表)

维度Vue 2 (Object.defineProperty)Vue 3 (Proxy)胜出方
劫持粒度属性级(必须先遍历 data)对象级(一次 Proxy 搞定整个对象)Vue 3
新增属性响应式不支持(需 Vue.set / this.$set)天然支持Vue 3
删除属性响应式不支持支持(deleteProperty 拦截)Vue 3
数组变化(push 等)需要重写 7 个数组方法直接拦截,无需重写Vue 3
嵌套对象响应式递归遍历所有属性(性能开销大)惰性递归(访问到才代理)Vue 3
性能(大数据对象)遍历 + 定义 getter/setter 开销大Proxy 创建快,拦截统一Vue 3
数据代理实现显式在 vm 上 defineProperty 转发基本不需要额外代理层(直接用 proxy 对象)Vue 3 更简洁
兼容性IE9+ES6+(现代浏览器)Vue 2

5. 手写极简版 Vue 数据代理(Vue 2 风格 + Vue 3 风格)

Vue 2 风格(数据代理 + 简单响应式)

class MiniVue {
  constructor(options) {
    this.$data = options.data()
    this.initProxy()
    // 简单观察者(实际 Vue 复杂得多)
    this.observe(this.$data)
  }

  initProxy() {
    Object.keys(this.$data).forEach(key => {
      Object.defineProperty(this, key, {
        get: () => this.$data[key],
        set: val => { this.$data[key] = val; console.log(`${key} 更新了!`) }
      })
    })
  }

  observe(obj) {
    Object.keys(obj).forEach(key => {
      let val = obj[key]
      Object.defineProperty(obj, key, {
        get: () => val,
        set: newVal => { val = newVal; console.log('数据劫持更新') }
      })
    })
  }
}

const app = new MiniVue({
  data() { return { count: 0 } }
})

app.count = 100   // 触发代理 set + 数据劫持 set

Vue 3 风格(直接用 Proxy)

function reactive(obj) {
  return new Proxy(obj, {
    set(target, key, val) {
      console.log(`${key} 被修改为 ${val}`)
      target[key] = val
      // 这里会 trigger 更新视图
      return true
    }
  })
}

const state = reactive({ count: 0, user: { name: '重阳' } })

state.count = 999            // 拦截
state.user.name = '重阳666'  // 惰性代理后也能拦截

总结一句话

  • Vue 2:数据代理 = 在 vm 实例上用 defineProperty 做属性转发 + 响应式劫持
  • Vue 3:数据代理的必要性大幅降低,因为 Proxy 本身就是强大的“代理对象”,直接用它就实现了响应式 + 访问便利

重阳,你现在用的是 Vue 2 还是 Vue 3 项目?
想继续深挖:

  • 手写更完整的响应式系统(带 dep + watcher)?
  • Proxy 的 13 种拦截器在 Vue 里分别怎么用?
  • ref vs reactive 的代理区别?
  • Vue 3 Proxy 的性能优势具体数据?

随时告诉我,我们继续拆!

文章已创建 4357

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

相关文章

开始在上面输入您的搜索词,然后按回车进行搜索。按ESC取消。

返回顶部