JavaScript 中的属性劫持与数据响应式原理
字数 1693 2025-12-13 03:45:55
JavaScript 中的属性劫持与数据响应式原理
1. 描述
属性劫持是数据响应式系统的核心机制,它通过拦截对象的属性访问和修改操作,在属性被读取或设置时执行自定义逻辑(如依赖收集和派发更新)。Vue 2.x 使用 Object.defineProperty 实现,而 Vue 3+ 及现代框架转向使用 Proxy 以获得更强大的拦截能力。理解这一原理是掌握前端框架响应式系统的关键。
2. 核心概念:数据响应式
数据响应式是指当数据变化时,依赖该数据的代码(如视图渲染函数)能自动重新执行。这需要解决两个问题:
- 依赖收集:追踪哪些代码依赖了当前数据。
- 派发更新:当数据变化时,通知所有依赖的代码。
3. Object.defineProperty 的实现方式
这是 Vue 2.x 采用的方案,通过定义对象的 getter 和 setter 来拦截操作。
步骤 1:基本拦截
let data = { count: 0 };
let value = data.count;
Object.defineProperty(data, 'count', {
get() {
console.log('读取 count');
return value;
},
set(newValue) {
console.log('设置 count');
value = newValue;
}
});
console.log(data.count); // 输出:读取 count, 0
data.count = 1; // 输出:设置 count
注意:这里使用外部变量 value 存储实际值,避免在 getter/setter 中直接访问 this.count 导致递归调用。
步骤 2:依赖收集与派发更新
实现一个简易的响应式系统,包含一个“依赖”类来管理订阅者。
class Dep {
constructor() {
this.subscribers = new Set(); // 存储依赖函数
}
depend() {
if (activeEffect) {
this.subscribers.add(activeEffect);
}
}
notify() {
this.subscribers.forEach(effect => effect());
}
}
let activeEffect = null; // 当前正在执行的依赖函数
function watchEffect(effect) {
activeEffect = effect;
effect(); // 首次执行,触发 getter 收集依赖
activeEffect = null;
}
function defineReactive(obj, key, val) {
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
dep.depend(); // 收集依赖
return val;
},
set(newVal) {
if (newVal !== val) {
val = newVal;
dep.notify(); // 派发更新
}
}
});
}
const data = { count: 0 };
defineReactive(data, 'count', data.count);
// 使用示例
watchEffect(() => {
console.log(`count 是: ${data.count}`);
});
// 输出:count 是: 0
data.count = 5; // 输出:count 是: 5
data.count = 10; // 输出:count 是: 10
说明:
Dep类:每个属性对应一个 Dep 实例,管理其所有依赖(副作用函数)。watchEffect:注册副作用函数,执行时设置activeEffect,以便 getter 中能收集到它。defineReactive:将属性转换为响应式,getter 调用dep.depend()收集当前活跃的依赖,setter 调用dep.notify()触发所有依赖重新执行。
步骤 3:处理嵌套对象和数组
Object.defineProperty 的局限性在于:
- 无法检测新增/删除的属性(Vue 2.x 需用
Vue.set/Vue.delete)。 - 对数组的索引修改、长度修改无法拦截(Vue 2.x 重写了数组的 push/pop 等方法)。
实现嵌套响应式需要递归:
function observe(obj) {
if (typeof obj !== 'object' || obj === null) return;
Object.keys(obj).forEach(key => {
let value = obj[key];
observe(value); // 递归处理嵌套对象
defineReactive(obj, key, value);
});
}
4. Proxy 的实现方式
Proxy 是 ES6 新增的元编程特性,能拦截整个对象的操作,弥补了 Object.defineProperty 的不足。
步骤 1:基本拦截
const data = { count: 0 };
const handler = {
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);
}
};
const proxy = new Proxy(data, handler);
console.log(proxy.count); // 输出:读取 count, 0
proxy.count = 1; // 输出:设置 count 为 1
Reflect 方法用于执行默认行为,保持与原始对象的一致性。
步骤 2:实现响应式系统
结合 Proxy 和依赖收集:
const targetMap = new WeakMap(); // 存储对象 -> 键 -> 依赖映射
let activeEffect = null;
function track(target, key) {
if (!activeEffect) return;
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) {
dep = new Set();
depsMap.set(key, dep);
}
dep.add(activeEffect);
}
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(key);
if (dep) {
dep.forEach(effect => effect());
}
}
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
track(target, key);
const res = Reflect.get(target, key, receiver);
if (typeof res === 'object' && res !== null) {
return reactive(res); // 深层响应式
}
return res;
},
set(target, key, value, receiver) {
const oldValue = target[key];
const res = Reflect.set(target, key, value, receiver);
if (oldValue !== value) {
trigger(target, key);
}
return res;
},
deleteProperty(target, key) {
const hadKey = key in target;
const res = Reflect.deleteProperty(target, key);
if (hadKey) {
trigger(target, key);
}
return res;
}
});
}
// 使用示例
const state = reactive({ count: 0, nested: { foo: 1 } });
watchEffect(() => {
console.log(`count: ${state.count}, nested.foo: ${state.nested.foo}`);
});
// 输出:count: 0, nested.foo: 1
state.count = 2; // 输出:count: 2, nested.foo: 1
state.nested.foo = 3; // 输出:count: 2, nested.foo: 3
state.newProp = 4; // 新增属性也能触发,输出:count: 2, nested.foo: 3
优势:
- 可拦截新增/删除属性、数组索引修改等。
- 无需递归遍历初始对象,只在访问时递归代理(惰性代理)。
- 支持更多拦截类型(如
in、delete)。
5. 对比与总结
| 特性 | Object.defineProperty | Proxy |
|---|---|---|
| 拦截能力 | 仅限 get/set | 多种操作(get, set, delete, has 等) |
| 数组拦截 | 需重写方法 | 直接支持索引修改 |
| 新增属性 | 无法检测 | 可检测 |
| 性能 | 初始化时递归遍历 | 惰性代理,按需递归 |
| 兼容性 | IE9+ | ES6+(现代浏览器) |
| 深层响应式 | 需递归遍历初始化 | 访问时递归代理 |
6. 实际应用注意点
- 循环引用处理:需用
WeakMap缓存已代理对象,避免重复代理和无限递归。 - 性能优化:Proxy 的惰性代理更高效,但频繁访问嵌套属性时可能产生多个代理对象,需权衡内存。
- 不可变数据:结合
Object.freeze避免意外修改非响应式部分。
7. 扩展:Vue 3 的响应式系统
Vue 3 的 reactive 基于 Proxy,ref 通过 .value 属性访问实现响应式,并利用 effect 函数进行依赖追踪,核心原理与本例相似但更复杂(如调度器、清理机制)。
通过以上步骤,你可以理解属性劫持如何实现数据响应式,这是现代前端框架的核心机制之一。