下面是一个基于 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缓存已代理对象,避免对同一对象重复创建 Proxyeffect支持嵌套(用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。