JavaScript Proxy 拦截对象操作实现响应式系统
响应式系统是现代前端框架的核心能力之一。当你修改数据时,视图自动更新;当你订阅状态变化时,界面实时响应。Vue 3 的响应式系统正是基于 Proxy 实现的,这篇文章将带你从零构建一个完整的响应式系统。
理解 Proxy 的拦截机制
Proxy 是 ES6 引入的元编程特性,它能在对象操作的关键节点拦截并自定义行为。你可以把它想象成一个"透明代理"——外界对对象的所有操作,都会先经过这层代理的"审核"。
const target = { name: '小明' };
const proxy = new Proxy(target, {
// 读取属性时触发
get(target, property, receiver) {
console.log(`读取属性 ${property}`);
return Reflect.get(target, property, receiver);
},
// 设置属性时触发
set(target, property, value, receiver) {
console.log(`设置属性 ${property} = ${value}`);
return Reflect.set(target, property, value, receiver);
}
});
proxy.name; // 输出:读取属性 name
proxy.name = '小红'; // 输出:设置属性 name = 小红
```
**核心拦截方法一览**:
| 拦截方法 | 触发时机 | 常用场景 |
| :--- | :--- | :--- |
| `get` | 读取属性值 | 依赖收集 |
| `set` | 设置属性值 | 触发更新 |
| `has` | 使用 `in` 运算符 | 监听属性存在性 |
| `deleteProperty` | 使用 `delete` 删除属性 | 监听属性删除 |
| `ownKeys` | 获取所有属性键 | 监听遍历操作 |
---
## 设计响应式系统的核心架构
一个完整的响应式系统包含三个核心角色:
**依赖(Dep)**:负责收集和通知哪些地方使用了某个响应式数据。每个响应式属性都有自己独立的依赖管理器。
**订阅者(Subscriber/Watcher)**:当依赖变化时,需要被通知更新的函数或组件。比如组件的更新函数、计算属性的重新计算函数。
**响应式函数(Reactive)**:将普通对象转换为响应式对象的核心函数。所有对响应式对象的读写都会经过拦截,从而触发依赖收集或更新通知。
**工作流程**:读取属性时,当前活跃的订阅者自动注册到该属性的依赖中;设置属性时,该属性的所有订阅者被依次唤醒执行更新。
---
## 第一步:实现依赖管理器
依赖管理器需要维护一个订阅者列表,并提供收集依赖和通知更新的方法。
```javascript
class Dep {
constructor() {
// 使用 Set 避免重复订阅
this.subscribers = new Set();
}
// 添加订阅者
depend() {
if (activeSubscriber) {
this.subscribers.add(activeSubscriber);
}
}
// 通知所有订阅者更新
notify() {
this.subscribers.forEach(subscriber => subscriber());
}
}
// 全局变量:记录当前正在收集依赖的订阅者
let activeSubscriber = null;
// 订阅者封装函数
function watch(fn) {
activeSubscriber = fn;
fn(); // 执行一次,触发依赖收集
activeSubscriber = null;
}
```
---
## 第二步:实现响应式转换函数
接下来,编写将普通对象转换为响应式对象的函数。这个函数会遍历对象的所有属性,为每个属性创建独立的依赖管理器,并使用 `Proxy` 拦截所有操作。
```javascript
function reactive(target) {
// 创建依赖映射表
const depMap = new WeakMap();
// 获取或创建某个属性的依赖管理器
function getDep(target, property) {
if (!depMap.has(target)) {
depMap.set(target, new Map());
}
const propertyDeps = depMap.get(target);
if (!propertyDeps.has(property)) {
propertyDeps.set(property, new Dep());
}
return propertyDeps.get(property);
}
return new Proxy(target, {
get(target, property, receiver) {
const dep = getDep(target, property);
// 收集依赖
dep.depend();
return Reflect.get(target, property, receiver);
},
set(target, property, value, receiver) {
const dep = getDep(target, property);
// 先设置新值
const result = Reflect.set(target, property, value, receiver);
// 再通知更新
dep.notify();
return result;
},
deleteProperty(target, property) {
const dep = getDep(target, property);
const result = Reflect.deleteProperty(target, property);
if (result) {
dep.notify();
}
return result;
}
});
}
```
`WeakMap` 作为外部的依赖映射表,确保当对象不再被引用时可以正确被垃圾回收,避免内存泄漏。
---
## 第三步:完整应用示例
现在将所有组件组合起来,构建一个可直接运行的响应式系统示例。
```javascript
// ==================== 完整代码 ====================
class Dep {
constructor() {
this.subscribers = new Set();
}
depend() {
if (activeSubscriber) {
this.subscribers.add(activeSubscriber);
}
}
notify() {
this.subscribers.forEach(subscriber => subscriber());
}
}
let activeSubscriber = null;
function watch(fn) {
activeSubscriber = fn;
fn();
activeSubscriber = null;
}
function reactive(target) {
const depMap = new WeakMap();
function getDep(target, property) {
if (!depMap.has(target)) {
depMap.set(target, new Map());
}
const propertyDeps = depMap.get(target);
if (!propertyDeps.has(property)) {
propertyDeps.set(property, new Dep());
}
return propertyDeps.get(property);
}
return new Proxy(target, {
get(target, property, receiver) {
const dep = getDep(target, property);
dep.depend();
return Reflect.get(target, property, receiver);
},
set(target, property, value, receiver) {
const dep = getDep(target, property);
const result = Reflect.set(target, property, value, receiver);
dep.notify();
return result;
},
deleteProperty(target, property) {
const dep = getDep(target, property);
const result = Reflect.deleteProperty(target, property);
if (result) {
dep.notify();
}
return result;
}
});
}
// ==================== 使用示例 ====================
// 创建响应式状态
const state = reactive({
count: 0,
message: 'Hello'
});
// 模拟视图更新函数
function render() {
console.log(`[视图更新] count = ${state.count}, message = "${state.message}"`);
}
// 订阅状态变化
watch(render); // 初始执行一次
// 修改状态,触发视图更新
console.log('--- 第一次修改 ---');
state.count++;
console.log('--- 第二次修改 ---');
state.message = 'World';
console.log('--- 第三次修改 ---');
state.count = 10;
```
**执行结果**:
```text
--- 第一次修改 ---
[视图更新] count = 1, message = "Hello"
--- 第二次修改 ---
[视图更新] count = 1, message = "World"
--- 第三次修改 ---
[视图更新] count = 10, message = "World"
```
每当修改 `state` 的任意属性,所有订阅了该状态的函数都会自动重新执行,实现了"数据驱动视图"的核心效果。
---
## 第四步:支持嵌套对象的响应式
上面的实现只处理了对象的直接属性。如果属性值是嵌套对象,需要递归将其也转换为响应式,才能实现深度响应。
```javascript
function reactive(target) {
const depMap = new WeakMap();
function getDep(target, property) {
if (!depMap.has(target)) {
depMap.set(target, new Map());
}
if (!depMap.get(target).has(property)) {
depMap.get(target).set(property, new Dep());
}
return depMap.get(target).get(property);
}
return new Proxy(target, {
get(target, property, receiver) {
const dep = getDep(target, property);
dep.depend();
const value = Reflect.get(target, property, receiver);
// 递归处理嵌套对象
if (value !== null && typeof value === 'object') {
return reactive(value);
}
return value;
},
set(target, property, value, receiver) {
const dep = getDep(target, property);
const result = Reflect.set(target, property, value, receiver);
dep.notify();
return result;
},
deleteProperty(target, property) {
const dep = getDep(target, property);
const result = Reflect.deleteProperty(target, property);
if (result) {
dep.notify();
}
return result;
}
});
}
```
**测试嵌套对象**:
```javascript
const user = reactive({
profile: {
name: '张三',
age: 25
}
});
watch(() => {
console.log(`姓名: ${user.profile.name}`);
});
console.log('--- 修改嵌套属性 ---');
user.profile.name = '李四';
第五步:处理数组的特殊性
数组的变异方法(如 push、splice)不会触发 set 拦截,因为这些方法是通过原型链调用的,而不是直接赋值。我们需要重写这些方法,确保任何数组操作都能触发响应。
function reactive(target) {
if (Array.isArray(target)) {
// 处理数组:重写变异方法
const originalMethods = target.map(method => target[method].bind(target));
const methods = ['push', 'pop', 'shift', 'unshift', 'splice', 'reverse', 'sort'];
methods.forEach((method, index) => {
target[method] = function(...args) {
const result = originalMethods[index](...args);
// 触发依赖通知(简化版:实际需要更精细的依赖管理)
target.dep?.notify();
return result;
};
});
target.dep = new Dep();
}
const depMap = new WeakMap();
function getDep(target, property) {
if (!depMap.has(target)) {
depMap.set(target, new Map());
}
if (!depMap.get(target).has(property)) {
depMap.get(target).set(property, new Dep());
}
return depMap.get(target).get(property);
}
return new Proxy(target, {
get(target, property, receiver) {
const dep = getDep(target, property);
dep.depend();
const value = Reflect.get(target, property, receiver);
if (value !== null && typeof value === 'object') {
return reactive(value);
}
return value;
},
set(target, property, value, receiver) {
const dep = getDep(target, property);
const result = Reflect.set(target, property, value, receiver);
dep.notify();
return result;
},
deleteProperty(target, property) {
const dep = getDep(target, property);
const result = Reflect.deleteProperty(target, property);
if (result) {
dep.notify();
}
return result;
}
});
}
性能优化与工程实践
在生产环境中,响应式系统还需要考虑以下优化点:
按需收集依赖:使用 cleanup 机制在订阅者重新执行前,清除旧的依赖关系,避免内存泄漏和不必要的更新。
避免重复代理:对已经代理过的对象返回同一个代理实例,可以使用 WeakMap 缓存已经代理过的对象。
调度器控制:当一次事件循环中多次修改数据时,可以使用队列批量更新,避免重复渲染。queueJob 函数可以将更新任务加入微任务队列。
只读视图:创建只读的代理视图,禁止修改操作,进一步提升代码健壮性。
function readonly(target) {
return new Proxy(target, {
set() {
console.warn('禁止修改只读对象');
return false;
},
deleteProperty() {
console.warn('禁止删除只读属性');
return false;
}
});
}
总结
通过这篇文章,你掌握了使用 JavaScript Proxy 实现响应式系统的完整方案。核心思路是利用 Proxy 拦截对象的 get 和 set 操作,在读取时收集依赖,在修改时通知更新。整个系统由依赖管理器、订阅者封装和响应式转换函数三部分组成,配合嵌套对象递归处理和数组方法重写,就能构建一个功能完整的响应式系统。

暂无评论,快来抢沙发吧!