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

下面是一个基于 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

相关推荐
linux_cfan7 小时前
videojs v10 源代码系列解读:06 · DOM 工具箱:事件、聚焦、Shadow DOM、定位
前端
尾善爱看海7 小时前
Vue 面试进阶篇:Composition API、插槽、自定义指令……8 个章节 + 高频面试题全解析
前端·javascript·vue.js·面试·vue
SEO_juper7 小时前
外贸多语言站最隐蔽的流量杀手:hreflang 错了,谷歌把德语页推给美国人(附审计脚本)
开发语言·前端·python·seo·独立站·谷歌优化
亿元程序员7 小时前
为什么现在 AI 这么发达了,还要坚持手搓教程?
前端
三小河8 小时前
在 Codex 桌面端接入 DeepSeek 模型(CC Switch 代理中转)
前端·javascript·人工智能
CoderYanger8 小时前
前端基础——JavaScript(WebAPI)代码案例
java·开发语言·前端·javascript·css·前端框架·html5
恋猫de小郭8 小时前
Shopify 从 React Native 回到 Swift/Kotlin,但是你以为有手就行??
android·前端·ios
BillKu9 小时前
Element Plus 的 el-tab-pane:lazy 的说明
前端·javascript·vue.js
爱勇宝9 小时前
《道德经》第 11 章:真正有用的,常常是你没写出来的那部分
前端·后端·产品
jearry9 小时前
WebView2 原生拖放的桥接之道:拆解 yyzTools 的 drop-zone.js
前端·c++