如何实现一个简化的响应式系统

下面是一个基于 Proxy 的简化响应式系统实现,涵盖依赖收集、触发更新、嵌套对象处理,以及一些常见边界处理(避免重复代理、Set/Delete 操作、数组索引等)。

javascript 复制代码
let activeEffect = null;
const effectStack = [];

const targetMap = new WeakMap();
const reactiveMap = new WeakMap();

const ITERATE_KEY = Symbol('iterate');

function effect(fn) {
  const effectFn = () => {
    cleanup(effectFn);
    activeEffect = effectFn;
    effectStack.push(effectFn);
    try {
      return fn();
    } finally {
      effectStack.pop();
      activeEffect = effectStack[effectStack.length - 1] || null;
    }
  };
  effectFn.deps = [];
  effectFn();
  return effectFn;
}

function cleanup(effectFn) {
  for (const dep of effectFn.deps) {
    dep.delete(effectFn);
  }
  effectFn.deps.length = 0;
}

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()));

  if (!dep.has(activeEffect)) {
    dep.add(activeEffect);
    activeEffect.deps.push(dep);
  }
}

function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;

  const effectsToRun = new Set();
  const add = (dep) => {
    if (!dep) return;
    dep.forEach((fn) => {
      if (fn !== activeEffect) effectsToRun.add(fn);
    });
  };

  add(depsMap.get(key));
  add(depsMap.get(ITERATE_KEY));

  effectsToRun.forEach((fn) => fn());
}

function isObject(val) {
  return val !== null && typeof val === 'object';
}

function createReactive(obj) {
  if (!isObject(obj)) return obj;
  if (reactiveMap.has(obj)) return reactiveMap.get(obj);

  const proxy = new Proxy(obj, {
    get(target, key, receiver) {
      if (key === '__isReactive__') return true;

      const res = Reflect.get(target, key, receiver);
      track(target, key);

      return isObject(res) ? createReactive(res) : res;
    },

    set(target, key, value, receiver) {
      const oldValue = target[key];
      const hadKey = Object.prototype.hasOwnProperty.call(target, key);
      const result = Reflect.set(target, key, value, receiver);

      if (!hadKey) {
        trigger(target, ITERATE_KEY);
        trigger(target, key);
      } else if (oldValue !== value && !(Number.isNaN(oldValue) && Number.isNaN(value))) {
        trigger(target, key);
      }
      return result;
    },

    has(target, key) {
      track(target, key);
      return Reflect.has(target, key);
    },

    ownKeys(target) {
      track(target, ITERATE_KEY);
      return Reflect.ownKeys(target);
    },

    deleteProperty(target, key) {
      const hadKey = Object.prototype.hasOwnProperty.call(target, key);
      const result = Reflect.deleteProperty(target, key);
      if (hadKey && result) {
        trigger(target, key);
        trigger(target, ITERATE_KEY);
      }
      return result;
    },
  });

  reactiveMap.set(obj, proxy);
  return proxy;
}

设计要点

1. 依赖收集 (track)

  • WeakMap → Map → Set 三层结构存依赖:target → key → effects
  • WeakMap 避免内存泄漏(target 被回收时依赖一起回收)
  • 只有在 activeEffect 存在时才收集,避免在非 effect 环境下 get 也被记录

2. 触发更新 (trigger)

  • 取出对应 key 的依赖集合并执行
  • 用一个新的 Set 包裹要执行的 effects,避免遍历过程中 cleanup + 重新收集导致的死循环
  • fn !== activeEffect 防止 effect 内自更新触发自身的递归

3. 嵌套对象处理 --- 懒代理

关键在 get 拦截器里:

javascript 复制代码
return isObject(res) ? createReactive(res) : res;

只有真正访问到嵌套对象时才递归代理它,比一次性深度遍历更高效,也能正确处理后续动态新增的对象属性。

4. 其他增强

  • reactiveMap 缓存已代理对象,避免对同一对象重复创建 Proxy
  • effect 支持嵌套(用 effectStack 维护当前 activeEffect)
  • cleanup 每次执行前清理旧依赖,解决条件分支 场景下的"过期依赖"问题(比如 state.show ? state.a : state.b
  • set 中区分新增 key修改 key :新增会触发 ITERATE_KEY(使 for...in / Object.keys 也能响应)
  • 处理 NaN !== NaN 的边界

使用示例

javascript 复制代码
const state = createReactive({
  count: 0,
  user: { name: 'Tom', age: 18 },
});

effect(() => {
  console.log('count:', state.count);
});

effect(() => {
  console.log('user name:', state.user.name);
});

state.count++;
state.user.name = 'Jerry';
state.user.age = 20;

输出:

yaml 复制代码
count: 0
user name: Tom
count: 1
user name: Jerry

注意 state.user.age = 20 没有触发任何 effect --- 因为没有 effect 依赖 age,这正是依赖收集精度的体现。

如果还想扩展,可以在此基础上加 computed(用 dirty 标志位 + scheduler)和 watch(包装一个收集依赖的 effect + scheduler 回调),核心机制就是上面这套 effect / track / trigger

相关推荐
码海扬帆:前端探索之旅17 小时前
深度定制 uni-combox:新增功能详解与实战指南
前端·vue.js·uni-app
谷雨不太卷17 小时前
进程的状态码
java·前端·算法
打小就很皮...17 小时前
基于 Python + LangChain + RAG 的知识检索系统实战
前端·langchain·embedding·rag
BJ-Giser17 小时前
Cesium 烟雾粒子特效
前端·可视化·cesium
空中海17 小时前
02 ArkTS 语言与工程规范
java·前端·spring
YJlio17 小时前
7.4.5 Windows 11 企业网络连接与网络重置实战:远程访问、本地策略与故障恢复
前端·chrome·windows·python·edge·机器人·django
Slow菜鸟17 小时前
Codex CLI 教程(五)| Skills 安装指南:面向 Java 全栈工程师打造个人 ECC(V1版)
大数据·前端·人工智能
Lee川17 小时前
打字机是怎么炼成的:Chat 流式输出深度解析
前端·后端·面试
前端若水17 小时前
过渡(transition)高级:贝塞尔曲线、硬件加速
前端·css·css3
Lee川18 小时前
Token 无感刷新与 Logout:前端安全会话管理实战
前端·后端·react.js