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

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

相关推荐
kyriewen112 小时前
项目做了一半想重写?这套前端架构让你少走3年弯路
前端·javascript·chrome·架构·ecmascript·html5
HashTang2 小时前
我用 Cloudflare Workers + GitHub Actions 做了个 2.5 刀/月的 AI 日报,代码开源了
前端·ai编程·aiops
老王以为2 小时前
前端重生之 - 前端视角下的 Python
前端·后端·python
饭后一颗花生米2 小时前
2026 AI加持下前端学习路线:从入门到进阶,高效突破核心竞争力
前端·人工智能·学习
五号厂房2 小时前
TypeScript 类型导入详解:import type 与 import {type}
前端
果然_2 小时前
为什么你的 PR 总是多出一堆奇怪的 commit?90% 的人都踩过这个 Git 坑
前端·git
xpyjs2 小时前
零依赖、链式调用、可变对象:重新设计 JavaScript 颜色处理体验
前端
WayneYang2 小时前
Node.js 全栈知识点详细整理(含代码示例 + 前端结合实战)
前端·node.js
土拨鼠爱coding2 小时前
Chrome插件 - DIY Theme
前端·chrome