引言:什么是响应式编程?
在 JavaScript 中,响应式编程(Reactive Programming) 是一种编程范式,其核心思想是:当数据发生变化时,依赖该数据的其他部分能够自动更新。
这种"自动同步"机制,使得开发者无需手动追踪状态变化并更新视图,极大地提升了开发效率和代码可维护性。
本文将从 JavaScript 的角度,深入剖析响应式系统的实现原理,对比 Object.defineProperty
与 Proxy
的优劣,并构建一个高性能、可扩展的响应式系统。
第一部分:Object.defineProperty
------ ES5 的响应式基石
1.1 什么是 Object.defineProperty
?
Object.defineProperty
是 ES5 引入的 API,用于精确控制对象属性的行为。它允许我们定义属性的 getter 和 setter,从而"劫持"属性的读取与赋值操作。
语法:
js
Object.defineProperty(obj, prop, descriptor)
核心描述符:
描述符 | 说明 |
---|---|
get |
属性被读取时调用的函数 |
set |
属性被赋值时调用的函数 |
enumerable |
是否可枚举(出现在 for...in 中) |
configurable |
是否可配置(如删除属性) |
⚠️ 注意:
get/set
不能与value/writable
共存。
1.2 使用 defineProperty
实现基础响应式
目标:
当对象属性变化时,自动执行副作用函数(如更新 DOM)。
HTML:
html
<div id="app">0</div>
<button id="btn">+1</button>
JavaScript:
js
let data = {
count: 0
};
// 存储副作用函数
const effects = new Set();
// 注册副作用
function watchEffect(fn) {
effects.add(fn);
fn(); // 立即执行一次,触发 getter 收集依赖
}
// 定义响应式属性
function defineReactive(obj, key, val) {
// 递归处理嵌套对象
if (typeof val === 'object' && val !== null) {
observe(val);
}
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
console.log(`读取 ${key}`);
return val;
},
set(newVal) {
if (newVal === val) return;
val = newVal;
// 触发所有副作用
effects.forEach(effect => effect());
}
});
}
function observe(obj) {
if (typeof obj !== 'object' || obj === null) return;
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key]);
});
}
// 初始化响应式
observe(data);
// 定义副作用
watchEffect(() => {
document.getElementById('app').textContent = data.count;
});
// 测试
document.getElementById('btn').addEventListener('click', () => {
data.count++;
});
1.3 defineProperty
的致命缺陷
尽管功能强大,但 Object.defineProperty
存在多个根本性问题:
1. 无法监听数组索引变化
js
data.arr = [1, 2, 3];
data.arr[0] = 99; // ❌ 不触发 set
data.arr.push(4); // ❌ 不是赋值操作
解决方案 :重写数组的变异方法(
push
,pop
,splice
等)。
2. 无法监听动态添加的属性
js
data.newProp = 'hello'; // ❌ 不响应
解决方案 :提供
$set
方法手动定义响应式属性。
3. 无法拦截 delete
操作
js
delete data.count; // ❌ 无法监听
解决方案 :使用
$delete
。
4. 初始化性能差
需要递归遍历整个对象,对深层嵌套对象性能影响大。
5. 语法繁琐
每个属性都要单独 defineProperty
,代码冗长。
第二部分:Proxy
------ ES6 的现代响应式方案
2.1 什么是 Proxy
?
Proxy
是 ES6 提供的元编程工具,用于创建对象的代理,拦截并自定义其所有操作。
语法:
js
const proxy = new Proxy(target, handler);
常用陷阱(Traps):
陷阱 | 触发时机 |
---|---|
get(target, property) |
读取属性 |
set(target, property, value) |
设置属性 |
has(target, property) |
in 操作符 |
deleteProperty(target, property) |
delete 操作 |
ownKeys(target) |
Object.keys() 等枚举操作 |
2.2 使用 Proxy
实现响应式系统
js
// 存储依赖映射:target -> key -> effects
const targetMap = new WeakMap();
// 当前活跃的副作用函数
let activeEffect = null;
// 收集依赖
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) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(key);
if (dep) {
// 避免递归调用导致无限循环
dep.forEach(effect => {
if (effect !== activeEffect) {
effect();
}
});
}
}
// 创建响应式代理
function reactive(target) {
// 基本类型不代理
if (typeof target !== 'object' || target === null) {
return target;
}
// 已有代理则返回
if (target.__v_raw) {
return target;
}
// 避免重复代理
if (target.__v_proxy) {
return target.__v_proxy;
}
const handler = {
get(target, key, receiver) {
// 特殊处理:返回原始对象或代理对象
if (key === '__v_raw') return target;
if (key === '__v_proxy') return receiver;
const result = Reflect.get(target, key, receiver);
track(target, key); // 收集依赖
// 深层代理:嵌套对象也变为响应式
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);
// 仅当值真正变化时才触发更新
if (oldValue !== value) {
trigger(target, key);
}
return result;
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key);
if (result) {
trigger(target, key);
}
return result;
}
};
const proxy = new Proxy(target, handler);
// 保存代理引用
target.__v_proxy = proxy;
return proxy;
}
// 注册副作用
function watchEffect(fn) {
const effect = () => {
activeEffect = effect;
try {
fn();
} finally {
activeEffect = null;
}
};
effect();
return effect;
}
2.3 Proxy
的五大优势
1. 可代理整个对象
无需逐个属性定义,代码更简洁。
2. 支持数组监听
js
proxy.arr.push(4); // ✅ 触发 set
proxy.arr[0] = 99; // ✅ 触发 set
3. 支持动态属性与 delete
js
proxy.newProp = 'hello'; // ✅ 响应
delete proxy.newProp; // ✅ 可拦截
4. 更好的性能
懒代理机制,按需创建嵌套代理,避免递归开销。
5. 支持更多操作拦截
如 in
、delete
、Object.keys()
等。
2.4 Proxy
的局限性
- 不支持 IE:无法通过 Babel 转译。
this
指向问题:通过原始对象调用方法时,不会经过代理。- 某些内置对象行为复杂 :如
Date
、Map
等。
第三部分:高级优化与最佳实践
3.1 依赖收集优化:WeakMap + Map + Set
我们使用:
WeakMap
:以原始对象为键,避免内存泄漏Map
:以属性名为键,存储依赖Set
:存储唯一的副作用函数
优势:
- 自动垃圾回收(WeakMap 的键是弱引用)
- 快速查找与去重
3.2 性能优化:懒代理与缓存
- 懒代理:只在访问嵌套对象时才创建代理
- 缓存:每个对象只代理一次,避免重复
js
if (target.__v_proxy) return target.__v_proxy;
3.3 处理 this
指向问题
当对象方法中使用 this
时,需确保 this
指向代理对象。
js
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver);
// 如果是函数,绑定 receiver(代理对象)作为 this
if (typeof result === 'function') {
return result.bind(receiver);
}
return result;
}
3.4 支持 readonly
与 shallowReactive
只读代理:
js
function readonly(target) {
return new Proxy(target, {
set() {
console.warn('Cannot set on readonly object');
return true;
},
deleteProperty() {
console.warn('Cannot delete from readonly object');
return true;
}
});
}
浅层响应式:
js
function shallowReactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
track(target, key);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
trigger(target, key);
return result;
}
});
}
3.5 错误处理与边界情况
- 处理
Symbol
类型的 key - 避免无限递归(如
proxy.a = proxy
) - 处理
NaN
比较
js
// 检查是否为 NaN
function hasChanged(value, oldValue) {
return value !== oldValue && (value === value || oldValue === oldValue);
}
第四部分:完整可运行示例
js
// 将上述所有代码整合
// ... (前面的 reactive, watchEffect 实现)
// 测试用例
const state = reactive({
count: 0,
user: {
name: 'Alice'
},
arr: [1, 2, 3]
});
// 副作用1:更新 DOM
watchEffect(() => {
document.getElementById('count').textContent = state.count;
document.getElementById('name').textContent = state.user.name;
document.getElementById('arr').textContent = state.arr.join(', ');
});
// 副作用2:日志
watchEffect(() => {
console.log('State changed:', state.count, state.user.name);
});
// 测试按钮
document.getElementById('inc').addEventListener('click', () => {
state.count++;
});
document.getElementById('changeName').addEventListener('click', () => {
state.user.name = 'Bob';
});
document.getElementById('push').addEventListener('click', () => {
state.arr.push(Math.random() * 100);
});
document.getElementById('dynamic').addEventListener('click', () => {
state.newProp = 'dynamic'; // 动态添加
});
html
<div>
<p>Count: <span id="count">0</span></p>
<p>Name: <span id="name">Alice</span></p>
<p>Array: <span id="arr">1, 2, 3</span></p>
<button id="inc">+1</button>
<button id="changeName">Change Name</button>
<button id="push">Push Random</button>
<button id="dynamic">Add Dynamic Prop</button>
</div>
第五部分:总结与展望
特性 | Object.defineProperty |
Proxy |
---|---|---|
数组监听 | ❌ 需重写方法 | ✅ 原生支持 |
动态属性 | ❌ 无法监听 | ✅ 支持 |
delete 操作 |
❌ 无法拦截 | ✅ 支持 |
性能 | ❌ 递归初始化开销大 | ✅ 懒代理,性能优 |
兼容性 | ✅ 支持 IE9+ | ❌ 不支持 IE |
代码复杂度 | ❌ 繁琐 | ✅ 简洁 |
结论:
Proxy
是现代 JavaScript 响应式系统的首选方案 ,它解决了defineProperty
的几乎所有缺陷。- 在不考虑 IE 的现代项目中,应优先使用
Proxy
。 - 对于需要兼容旧浏览器的项目,可采用
defineProperty
+ 数组方法重写的方式。
响应式系统是现代前端框架的核心,理解其底层原理,有助于你成为更高级的 JavaScript 开发者。