Object.defineProperty/Proxy与 vue2 + vue3 响应式原理

一、Object.defineProperty

基本用法

Object.defineProperty(obj, prop, descriptor) 可以精确地定义或修改对象上的属性,其中 getset 是拦截数据读取与写入的关键。

javascript 复制代码
let obj = {};
let internalValue = 0;
Object.defineProperty(obj, 'count', {
  get() {
    console.log('读取 count');
    return internalValue;
  },
  set(val) {
    console.log('设置 count:', val);
    internalValue = val;
  }
});
obj.count;  // 触发 get
obj.count = 5; // 触发 set

实现响应式的核心思路

利用 get 收集依赖(谁用到了这个属性),set 触发更新(通知所有依赖重新执行)。

致命缺陷

  1. 无法监听对象新增/删除属性
    defineProperty 只能劫持已存在的属性,新增 obj.newProp = 1 无法被感知,必须使用 Vue.set/delete 这种补丁手段。
  2. 无法监听数组索引和 length 变化
    通过下标修改数组(arr[0] = 1)或修改 length 都无法触发 setter。
  3. 必须深度遍历 + 递归代理
    在初始化时需要对 data 对象的所有层次的所有属性递归设置 getter/setter,遇到深层或大型对象会严重影响启动性能。
  4. 无法原生支持 Map、Set、WeakMap、WeakSet

二、ES6 Proxy

基本用法

new Proxy(target, handler) 创建一个对象的代理,handler 中可以定义多达 13 种拦截操作 ,包括 getsethasdeletePropertyownKeys 等。

javascript 复制代码
const target = { count: 0 };
const proxy = new Proxy(target, {
  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);
  },
  deleteProperty(target, key) {
    console.log(`删除 ${key}`);
    return Reflect.deleteProperty(target, key);
  }
});
proxy.count;          // 读取 count
proxy.newProp = 10;   // 设置 newProp = 10
delete proxy.newProp; // 删除 newProp

相较 defineProperty 的绝对优势

能力 defineProperty Proxy
监听新增/删除属性 ❌ 需要 Vue.set ✅ 可拦截 set / deleteProperty
监听数组下标和 length
监听 Map、Set、WeakMap
惰性代理(用到时才递归) ❌ 必须深度遍历 ✅ 仅在 get 时递归代理
劫持整个对象 属性级别 对象级别,更加灵活
性能 初始化时递归遍历全部属性 惰性递归,初始化极快

三、Vue2 响应式原理深度解析

Vue2 的响应式系统完全是建立在 Object.defineProperty 之上的,核心流程如下:

1. Observer(观察者)------ 将数据变为响应式

javascript 复制代码
class Observer {
  constructor(value) {
    this.walk(value);
  }
  walk(obj) {
    Object.keys(obj).forEach(key => defineReactive(obj, key, obj[key]));
  }
}
function defineReactive(obj, key, val) {
  const dep = new Dep();  // 每个属性一个依赖管理器
  // 递归处理嵌套对象
  if (typeof val === 'object' && val !== null) {
    new Observer(val);
  }
  Object.defineProperty(obj, key, {
    get() {
      if (Dep.target) {
        dep.depend();  // 收集当前 Watcher
      }
      return val;
    },
    set(newVal) {
      if (newVal === val) return;
      val = newVal;
      // 新值如果是对象也要代理
      if (typeof newVal === 'object' && newVal !== null) {
        new Observer(newVal);
      }
      dep.notify();  // 通知所有 Watcher 更新
    }
  });
}

2. Dep(依赖管理器)

javascript 复制代码
class Dep {
  constructor() {
    this.subs = []; // 存储 Watcher
  }
  depend() {
    if (Dep.target) {
      Dep.target.addDep(this);
    }
  }
  notify() {
    this.subs.forEach(watcher => watcher.update());
  }
}

3. Watcher(观察者)

Watcher 代表一个依赖,比如组件的渲染函数、计算属性、watch 回调等。

javascript 复制代码
class Watcher {
  constructor(getter, callback) {
    this.getter = getter;
    this.callback = callback;
    this.get(); // 初始化时立刻求值,触发依赖收集
  }
  get() {
    Dep.target = this;
    this.value = this.getter();  // 执行 getter,触发数据属性的 getter,收集依赖
    Dep.target = null;
    return this.value;
  }
  update() {
    this.run();
  }
  run() {
    const oldValue = this.value;
    this.value = this.get();
    this.callback(this.value, oldValue);
  }
}

4. 数组的特殊处理

由于 defineProperty 无法监听数组索引/长度变化,Vue2 选择重写数组的变异方法push, pop, shift, unshift, splice, sort, reverse),在保持原有功能的同时手动触发通知。

javascript 复制代码
const arrayProto = Array.prototype;
const arrayMethods = Object.create(arrayProto);
['push','pop','shift','unshift','splice','sort','reverse'].forEach(method => {
  const original = arrayProto[method];
  Object.defineProperty(arrayMethods, method, {
    value(...args) {
      const result = original.apply(this, args);
      const ob = this.__ob__;  // Observer 实例
      ob.dep.notify();         // 手动通知
      return result;
    }
  });
});
// 在 Observer 中将数组的 __proto__ 指向 arrayMethods

5. Vue.set / Vue.delete

为解决新增/删除属性的问题,Vue2 额外提供了 Vue.set(obj, key, value),内部会调用 defineReactive 并通知更新。


四、Vue3 响应式原理深度解析

Vue3 全面拥抱 Proxy,并基于 Reflect 实现,其响应式系统更加简洁、强大。

1. reactive ------ 核心入口

javascript 复制代码
function reactive(target) {
  if (typeof target !== 'object' || target === null) return target;
  // 防止重复代理
  if (target.__v_isReactive) return target;
  const proxy = new Proxy(target, baseHandlers);
  proxy.__v_isReactive = true;
  return proxy;
}

2. baseHandlers(以 get/set 为例)

javascript 复制代码
const baseHandlers = {
  get(target, key, receiver) {
    // 如果是获取 raw 原始对象(如 toRaw),则返回 target
    if (key === '__v_raw') return target;
    const result = Reflect.get(target, key, receiver);
    // 依赖收集
    track(target, key);
    // 惰性递归:如果返回值是对象,继续用 reactive 包裹
    if (typeof result === 'object' && result !== null) {
      return reactive(result);
    }
    return result;
  },
  set(target, key, value, receiver) {
    const oldValue = target[key];
    const result = Reflect.set(target, key, value, receiver);
    // 触发更新
    trigger(target, key, value, oldValue);
    return result;
  },
  deleteProperty(target, key) {
    const hadKey = Object.prototype.hasOwnProperty.call(target, key);
    const result = Reflect.deleteProperty(target, key);
    if (hadKey) {
      trigger(target, key, undefined, 'delete');
    }
    return result;
  }
  // 还有 has, ownKeys 等拦截,也会触发 track/trigger
};

3. track 和 trigger ------ 依赖收集与触发

Vue3 使用 WeakMap<target, Map<key, Set<effect>>> 存储依赖关系,完全解耦了 Dep 类。

javascript 复制代码
const targetMap = new WeakMap();
let activeEffect = null;  // 类似 Dep.target

function track(target, key) {
  if (!activeEffect) return;
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()));
  }
  let dep = depsMap.get(key);
  if (!dep) {
    depsMap.set(key, (dep = new Set()));
  }
  dep.add(activeEffect);
}

function trigger(target, key, value, oldValue) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  const dep = depsMap.get(key);
  if (dep) {
    dep.forEach(effect => effect.run());
  }
}

4. effect ------ 代替 Watcher

javascript 复制代码
class ReactiveEffect {
  constructor(fn) {
    this.fn = fn;
  }
  run() {
    activeEffect = this;
    const result = this.fn();  // 执行函数,触发 getter 中的 track
    activeEffect = null;
    return result;
  }
}
function effect(fn) {
  const _effect = new ReactiveEffect(fn);
  _effect.run();  // 立即执行一次,收集依赖
  return _effect;
}

5. ref 的实现

针对基本类型,Vue3 使用一个包含 .value 的对象,内部仍是依赖追踪。

javascript 复制代码
function ref(value) {
  const obj = {
    get value() {
      track(obj, 'value');
      return value;
    },
    set value(newValue) {
      if (newValue !== value) {
        value = newValue;
        trigger(obj, 'value');
      }
    }
  };
  return obj;
}
// 或直接使用 reactive 包裹 { value }

6. computed 和 watch

computed 本质是一个惰性 effect,内部有 _dirty 标记实现缓存;watch 则是在 effect 基础上监听特定响应式对象的变化。


五、Vue2 vs Vue3 响应式核心差异对比

维度 Vue2 Vue3
数据劫持方式 Object.defineProperty(属性级别) Proxy(对象级别)
新增/删除属性 无法自动监听,需要 Vue.set/delete 原生支持(deleteProperty / set)
数组监听 重写数组方法,下标/长度无法监听 完美支持下标、length、及所有方法
Map/Set/WeakMap 不支持 原生支持
初始化性能 深度递归遍历所有属性,一次性开销大 惰性递归(get 时才代理),初始化极快
内存开销 每个属性生成 getter/setter + Dep,深层对象内存大 每个对象一个 Proxy,依赖关系存于 WeakMap,内存更可控
API 设计 基于组件选项的 data、computed、watch,耦合度高 独立响应式 API(reactive, ref, computed 等),可脱离组件使用
Reflect 使用 全面使用 Reflect,确保 this 正确和返回值布尔

六、总结

  • Object.defineProperty 凭借其精准的属性劫持能力成为 Vue2 的基石,但因设计上的硬伤(数组、属性增删),不得不借助补丁手段,导致响应式系统存在诸多限制。
  • Proxy 则是 ES6 带来的革命性元编程能力,能够劫持一个对象的几乎全部操作,使得响应式系统变得完整、自然、高效 。Vue3 的全新响应式架构正是建立于此,配合 track/triggereffect,不仅彻底解决了 Vue2 遗留的难题,还将响应式逻辑从组件中抽离出来,实现了 Composition API 的灵活与强大。

理解这两种响应式核心机制,不仅能加深对 Vue 框架的认知,更是掌握现代前端数据驱动思想的必经之路。

相关推荐
JustHappy29 分钟前
我汇总了身边朋友的经历才发现,其实第一份实习是最难找的......
前端·后端·面试
星栈41 分钟前
Dioxus 的响应式系统:`Signal`、`Memo`、`Effect` 和异步状态到底该怎么分工
前端·前端框架
yingyima42 分钟前
Java 正则表达式:比你想象的更强大
前端
yuanyxh4 小时前
macOS 应用 - 纯对话生成
前端·macos·ai编程
大家的林语冰4 小时前
ES5 凉凉,Babel 8 正式发布,默认不再编译为 ES5 和 CJS......
前端·javascript·前端工程化
光影少年5 小时前
react批量更新、同步/异步更新场景
前端·react.js·掘金·金石计划
假如让我当三天老蒯5 小时前
模块化:ES Module 与 CommonJS 的区别
前端·面试
用户40950115773175 小时前
Private Forge v2.0 发布:12大前端业务场景技能系统
前端
weedsfly6 小时前
异步编程全景与事件循环——彻底搞懂 JS 执行机制
前端·javascript
用户059540174466 小时前
AI Agent记忆测试踩坑实录:Mock骗了我一周,Mem0+pytest一招破局
前端·css