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 框架的认知,更是掌握现代前端数据驱动思想的必经之路。

相关推荐
问心无愧051313 小时前
ctf show web入门259
android·前端·笔记
存在的五月雨13 小时前
Vue中的nextTick
javascript·vue.js·ecmascript
肉肉不吃 肉13 小时前
watch中为什么不能直接侦听响应式对象的属性
前端·javascript·vue.js
热爱Liunx的丘丘人14 小时前
Dockerfile 构建自定义 Nginx Web 服务镜像
运维·前端·nginx
Web打印14 小时前
2027年Web打印的几种方法
前端·pdf·web
匠在江湖14 小时前
通用轻量级密码/鉴权/ 秘钥算法(C语言)
前端
喵了几个咪14 小时前
吃透后台权限系统:从架构设计到 Vue3/React 双框架完整落地
前端·vue.js·react.js·权限系统
meilindehuzi_a14 小时前
深入浅出 JavaScript 核心:从底层内存与编译阶段彻底看透 var、let、const
开发语言·javascript·ecmascript
夜雪闻竹14 小时前
Tailwind CSS v4 + Vite:现代前端样式方案
前端·css·ai