【透彻讲解】Proxy 和 Object.defineProperty 的区别:数据代理 vs 数据劫持

前言

在 Vue2 和 Vue3 的响应式原理中,我们经常会听到"数据劫持"和"数据代理"这两个概念。它们分别对应着 Object.definePropertyProxy 这两种技术实现。本文将彻底解析它们的区别,通过代码示例、参数说明和使用场景对比,让你完全掌握这两种技术。

一、基本概念对比

特性 Object.defineProperty Proxy
ES版本 ES5 ES6
拦截维度 属性级别 对象级别
数组处理 需要特殊处理 原生支持
新增属性 需要手动监听 自动拦截
性能 较好 更优

二、Object.defineProperty(数据劫持)

1. 基本用法

js 复制代码
const obj = {};
let value = '初始值';

Object.defineProperty(obj, 'property', {
  get: function() {
    console.log('获取值');
    return value;
  },
  set: function(newValue) {
    console.log('设置值', newValue);
    value = newValue;
  }
});

obj.property; // 控制台输出"获取值"
obj.property = '新值'; // 控制台输出"设置值 新值"

2. 参数详解

js 复制代码
Object.defineProperty(obj, prop, descriptor)
  • obj:目标对象

  • prop:要定义或修改的属性名

  • descriptor:属性描述符,包含:

    • configurable:是否可删除,默认为 false
    • enumerable:是否可枚举,默认为 false
    • value:属性值
    • writable:是否可写
    • get:getter 函数
    • set:setter 函数

3. 实现简单响应式

js 复制代码
function observe(data) {
  if (!data || typeof data !== 'object') return;
  
  Object.keys(data).forEach(key => {
    let value = data[key];
    observe(value); // 递归子属性
    
    Object.defineProperty(data, key, {
      enumerable: true,
      configurable: true,
      get() {
        console.log(`获取 ${key}: ${value}`);
        return value;
      },
      set(newVal) {
        console.log(`设置 ${key}: ${newVal}`);
        if (value !== newVal) {
          value = newVal;
          observe(newVal); // 新值是对象时继续监听
        }
      }
    });
  });
}

const data = { name: '张三', info: { age: 20 } };
observe(data);

data.name; // 获取 name: 张三
data.info.age = 21; // 获取 info: [object Object] (注意:无法直接监听嵌套属性变化)

4. 局限性

  1. 无法检测对象属性的添加或删除
js 复制代码
 const obj = { a: 1 };
    observe(obj);
    obj.b = 2; // 无法监听新增属性
  1. 数组方法需要特殊处理
js 复制代码
    const arr = [1, 2, 3];
    observe(arr);
    arr.push(4); // 无法监听数组push操作
  1. 需要递归监听属性造成额外的性能开销

三、Proxy(数据代理)

1. 基本用法

js 复制代码
const target = {};
const handler = {
  get(target, prop, receiver) {
    console.log(`获取属性 ${prop}`);
    return Reflect.get(target, prop, receiver);
  },
  set(target, prop, value, receiver) {
    console.log(`设置属性 ${prop} 为 ${value}`);
    return Reflect.set(target, prop, value, receiver);
  }
};

const proxy = new Proxy(target, handler);
proxy.name = '李四'; // 设置属性 name 为 李四
console.log(proxy.name); // 获取属性 name → 李四

2. 参数详解

js 复制代码
const proxy = new Proxy(target, handler)
  • target:目标对象

  • handler:处理器对象,包含"陷阱"方法:

    • get(target, prop, receiver):拦截属性读取
    • set(target, prop, value, receiver):拦截属性设置
    • has(target, prop):拦截in操作符
    • deleteProperty(target, prop):拦截delete操作
    • 13种拦截操作

3. 实现完整响应式

js 复制代码
function reactive(data) {
  if (!data || typeof data !== 'object') return data;
  
  const handler = {
    get(target, key, receiver) {
      console.log(`获取 ${key}`);
      const result = Reflect.get(target, key, receiver);
      return reactive(result); // 递归代理嵌套对象
    },
    set(target, key, value, receiver) {
      console.log(`设置 ${key} 为 ${value}`);
      return Reflect.set(target, key, reactive(value), receiver);
    },
    deleteProperty(target, key) {
      console.log(`删除 ${key}`);
      return Reflect.deleteProperty(target, key);
    }
  };
  
  return new Proxy(data, handler);
}

const state = reactive({
  name: '王五',
  hobbies: ['篮球', '游泳']
});

state.hobbies.push('跑步'); 
// 获取 hobbies → 设置 2 为 跑步 → 设置 length 为 3

4. 优势体现

  1. 完美监听数组变化
js 复制代码
    const arr = reactive([1, 2, 3]);
    arr.push(4); // 能够正常拦截
  1. 自动监听新增属性
js 复制代码
    const obj = reactive({});
    obj.newProp = '新属性'; // 能够在set中正常拦截
  1. 支持更多拦截操作
js 复制代码
    const proxy = reactive({});
    delete proxy.name; // 可以拦截delete操作
  1. 无需递归遍历自身进行数据劫持

四、核心区别对比

1. 实现原理不同

  • Object.defineProperty:直接修改原对象属性描述符
  • Proxy:创建原对象的代理对象,不修改原对象

2. 拦截粒度不同

js 复制代码
// Object.defineProperty必须指定具体属性
Object.keys(obj).forEach(key => {
  Object.defineProperty(obj, key, {...});
});

// Proxy一次性拦截整个对象
new Proxy(obj, {
  get() {...},
  set() {...}
});

五、Vue2 和 Vue3 响应式原理对比

Vue2 实现

js 复制代码
// 简化版Vue2响应式原理
class Observer {
  constructor(value) {
    this.walk(value);
  }
  
  walk(obj) {
    Object.keys(obj).forEach(key => {
      defineReactive(obj, key, obj[key]);
    });
  }
}

function defineReactive(obj, key, val) {
  // 递归处理子属性
  observe(val);
  
  Object.defineProperty(obj, key, {
    get() {
      console.log(`获取 ${key}`);
      return val;
    },
    set(newVal) {
      if (val === newVal) return;
      console.log(`设置 ${key}`);
      val = newVal;
      observe(newVal);
    }
  });
}

Vue3 实现

js 复制代码
// 简化版Vue3响应式原理
function reactive(target) {
  return createReactiveObject(target);
}

function createReactiveObject(target) {
  const handler = {
    get(target, key, receiver) {
      track(target, key); // 依赖收集
      const result = Reflect.get(target, key, receiver);
      return isObject(result) ? reactive(result) : result;
    },
    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;
    }
    // 其他陷阱...
  };
  
  return new Proxy(target, handler);
}

七、总结

Vue3 改用 Proxy 实现响应式,解决了 Vue2 中的诸多限制,降低我们的心智负担

希望这篇博文能帮助你彻底理解 Proxy 和 Object.defineProperty 的区别!如果有任何问题,欢迎在评论区留言讨论。

相关推荐
明月与玄武2 小时前
Vue 3 高性能实践 全面提速剖析!
前端·javascript·vue.js
童先生6 小时前
Nginx + Vue/React 前端 + API:防止路径混淆漏洞与跨域问题实战分享
前端·vue.js·nginx
Stringzhua9 小时前
Vue数据的变更操作与表单数据的收集【6】
前端·javascript·vue.js
万少9 小时前
可可图片编辑 HarmonyOS 上架应用分享
前端·harmonyos
你的人类朋友9 小时前
git常见操作整理(持续更新)
前端·git·后端
无羡仙9 小时前
Webpack 核心实战:从零搭建支持热更新与 Babel 转译的现代前端环境
前端·webpack·前端框架
乐~~~9 小时前
el-date-picker type=daterange 日期范围限制
javascript·vue.js·elementui
你的人类朋友9 小时前
git中的Fast-Forward是什么?
前端·git·后端
初遇你时动了情9 小时前
uniapp vue3 ts自定义底部 tabbar菜单
前端·javascript·uni-app
JarvanMo10 小时前
天塌了?Flutter工程总监跑去苹果了?
前端