文章目录

JavaScript Proxy拦截对象操作实现响应式系统

发布于 2026-04-05 16:12:47 · 浏览 11 次 · 评论 0 条

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 = '李四';

第五步:处理数组的特殊性

数组的变异方法(如 pushsplice)不会触发 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 拦截对象的 getset 操作,在读取时收集依赖,在修改时通知更新。整个系统由依赖管理器、订阅者封装和响应式转换函数三部分组成,配合嵌套对象递归处理和数组方法重写,就能构建一个功能完整的响应式系统。

评论 (0)

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

扫一扫,手机查看

扫描上方二维码,在手机上查看本文