前言
在上一篇文章中,我们深入分析了 Vue 3 响应式系统的设计原理。理论虽好,但要真正理解,没有什么比亲手实现一遍更有效了。本文将用约 100 行代码,从零实现一个 mini 响应式系统,涵盖 reactive、ref、effect、computed 和 watch 五大核心 API。
一、目标设计
我们将实现以下 API:
const state = reactive({ count: 0, name: 'Vue' });
const num = ref(10);
effect(() => {
console.log('count is:', state.count);
});
const double = computed(() => state.count * 2);
watch(() => state.name, (newVal, oldVal) => {
console.log(`name changed: ${oldVal} -> ${newVal}`);
});
二、核心数据结构
2.1 依赖关系图
targetMap (WeakMap)
├── target1 (Map)
│ ├── key1 -> dep (Set<ReactiveEffect>)
│ └── key2 -> dep (Set<ReactiveEffect>)
└── target2 (Map)
└── key1 -> dep (Set<ReactiveEffect>)
targetMap:用 WeakMap 以对象为 key,避免内存泄漏- 每个对象的每个属性对应一个
dep(Set),存储所有依赖此属性的 effect
2.2 ReactiveEffect 类
class ReactiveEffect {
constructor(fn, scheduler) {
this.fn = fn;
this.scheduler = scheduler;
this.deps = []; // 存储 effect 被收集到的所有 dep,用于清理
this.active = true;
}
run() {
if (!this.active) {
return this.fn();
}
// 压栈
shouldTrack = true;
activeEffect = this;
cleanupEffect(this);
try {
return this.fn();
} finally {
// 出栈
activeEffect = this.parent;
shouldTrack = false;
}
}
stop() {
if (this.active) {
cleanupEffect(this);
this.active = false;
}
}
}
三、从零实现:一步步搭建
3.1 全局状态与依赖收集
let activeEffect = null;
let shouldTrack = true;
const targetMap = new WeakMap();
/**
* 依赖收集
*/
function track(target, key) {
if (!shouldTrack || activeEffect === null) return;
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) {
dep = new Set();
depsMap.set(key, dep);
}
trackEffects(dep);
}
/**
* 将当前 effect 加入 dep,并反向记录
*/
function trackEffects(dep) {
if (!dep.has(activeEffect)) {
dep.add(activeEffect);
activeEffect.deps.push(dep);
}
}
/**
* 派发更新
*/
function trigger(target, type, key, newValue) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
// 收集需要执行的所有 effect
const effects = new Set();
const dep = depsMap.get(key);
if (dep) {
dep.forEach(e => effects.add(e));
}
// 数组 length 修改时,触发所有索引 >= newValue 的 effect
if (type === 'add' && Array.isArray(target)) {
const lengthDep = depsMap.get('length');
if (lengthDep) {
lengthDep.forEach(e => effects.add(e));
}
}
effects.forEach(effect => {
if (effect !== activeEffect) { // 防止自我触发死循环
if (effect.scheduler) {
effect.scheduler();
} else {
effect.run();
}
}
});
}
3.2 cleanupEffect:依赖清理
function cleanupEffect(effect) {
effect.deps.forEach(dep => {
dep.delete(effect);
});
effect.deps.length = 0;
}
每次 effect 执行前清理旧依赖,执行时重新收集。这解决分支切换问题:
const state = reactive({ flag: true, a: 1, b: 2 });
effect(() => {
console.log(state.flag ? state.a : state.b);
});
state.flag = false;
// 重新收集依赖:现在只依赖 flag 和 b,不再依赖 a
state.a = 99; // 不会触发 effect
3.3 reactive:Proxy 代理对象
const reactiveMap = new WeakMap();
const RAW = Symbol('raw');
const baseHandlers = {
get(target, key, receiver) {
if (key === RAW) return target;
const result = Reflect.get(target, key, receiver);
// 依赖收集
track(target, key);
// 惰性代理嵌套对象
if (result !== null && typeof result === 'object') {
return reactive(result);
}
return result;
},
set(target, key, value, receiver) {
const oldValue = target[key];
const hadKey = Array.isArray(target)
? Number(key) < target.length
: Object.prototype.hasOwnProperty.call(target, key);
// 如果赋的值是响应式对象,取原始值
if (value && value[RAW]) {
value = value[RAW];
}
const result = Reflect.set(target, key, value, receiver);
// 确保是原始对象的操作(避免原型链触发)
if (target === receiver[RAW]) {
if (!hadKey) {
trigger(target, 'add', key, value);
} else if (value !== oldValue && (oldValue === oldValue || value === value)) {
trigger(target, 'set', key, value);
}
}
return result;
},
deleteProperty(target, key) {
const hadKey = Object.prototype.hasOwnProperty.call(target, key);
const result = Reflect.deleteProperty(target, key);
if (hadKey && result) {
trigger(target, 'delete', key);
}
return result;
},
has(target, key) {
const result = Reflect.has(target, key);
track(target, key);
return result;
},
ownKeys(target) {
track(target, Array.isArray(target) ? 'length' : Symbol('iterate'));
return Reflect.ownKeys(target);
}
};
function reactive(target) {
if (typeof target !== 'object' || target === null) return target;
// 如果已经是代理,直接返回
if (target[RAW]) return target;
// 缓存检查
if (reactiveMap.has(target)) {
return reactiveMap.get(target);
}
const proxy = new Proxy(target, baseHandlers);
reactiveMap.set(target, proxy);
return proxy;
}
3.4 ref:包装基本类型
class RefImpl {
constructor(value) {
this._rawValue = value;
this._value = convert(value);
this.dep = new Set();
this.__v_isRef = true;
}
get value() {
trackRefValue(this);
return this._value;
}
set value(newVal) {
if (newVal !== this._rawValue && (newVal === newVal || this._rawValue === this._rawValue)) {
this._rawValue = newVal;
this._value = convert(newVal);
triggerRefValue(this);
}
}
}
function convert(value) {
return typeof value === 'object' && value !== null ? reactive(value) : value;
}
function trackRefValue(ref) {
if (shouldTrack && activeEffect) {
trackEffects(ref.dep);
}
}
function triggerRefValue(ref) {
ref.dep.forEach(effect => {
if (effect !== activeEffect) {
if (effect.scheduler) {
effect.scheduler();
} else {
effect.run();
}
}
});
}
function ref(value) {
if (value && value.__v_isRef) return value;
return new RefImpl(value);
}
3.5 effect:副作用管理
function effect(fn, options = {}) {
const _effect = new ReactiveEffect(fn, options.scheduler);
if (!options.lazy) {
_effect.run();
}
const runner = _effect.run.bind(_effect);
runner.effect = _effect;
return runner;
}
3.6 computed:惰性计算
class ComputedRefImpl {
constructor(getter, setter) {
this._setter = setter;
this._dirty = true;
this._value = undefined;
this.dep = new Set();
this.__v_isRef = true;
this.effect = new ReactiveEffect(getter, () => {
// 依赖变化时不立即计算,标记为脏值
if (!this._dirty) {
this._dirty = true;
// 通知依赖 computed 的 effect
triggerRefValue(this);
}
});
}
get value() {
trackRefValue(this);
if (this._dirty) {
this._dirty = false;
this._value = this.effect.run();
}
return this._value;
}
set value(newVal) {
if (this._setter) {
this._setter(newVal);
}
}
}
function computed(getterOrOptions) {
let getter, setter;
if (typeof getterOrOptions === 'function') {
getter = getterOrOptions;
setter = () => {
console.warn('Write operation failed: computed value is readonly');
};
} else {
getter = getterOrOptions.get;
setter = getterOrOptions.set;
}
return new ComputedRefImpl(getter, setter);
}
3.7 watch:监听变化
function watch(source, cb, options = {}) {
let getter;
if (typeof source === 'function') {
getter = source;
} else if (source && source.__v_isRef) {
getter = () => source.value;
} else if (typeof source === 'object') {
getter = () => source;
}
let oldValue;
let cleanup;
const onCleanup = (fn) => {
cleanup = fn;
};
const job = () => {
if (cleanup) cleanup();
const newValue = effect.run();
cb(newValue, oldValue, onCleanup);
oldValue = newValue;
};
const effect = new ReactiveEffect(getter, job);
if (options.immediate) {
job();
} else {
oldValue = effect.run();
}
}
四、完整代码整合
将以上所有代码合并,总计约 200 行(含注释):
// ============ 全局状态 ============
let activeEffect = null;
let shouldTrack = false;
const targetMap = new WeakMap();
const reactiveMap = new WeakMap();
const RAW = Symbol('raw');
// ============ 依赖收集与触发 ============
function track(target, key) {
if (!shouldTrack || !activeEffect) return;
let depsMap = targetMap.get(target);
if (!depsMap) { depsMap = new Map(); targetMap.set(target, depsMap); }
let dep = depsMap.get(key);
if (!dep) { dep = new Set(); depsMap.set(key, dep); }
if (!dep.has(activeEffect)) { dep.add(activeEffect); activeEffect.deps.push(dep); }
}
function trigger(target, type, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(key);
const effects = new Set();
if (dep) dep.forEach(e => e !== activeEffect && effects.add(e));
effects.forEach(e => e.scheduler ? e.scheduler() : e.run());
}
// ============ ReactiveEffect ============
class ReactiveEffect {
constructor(fn, scheduler) {
this.fn = fn; this.scheduler = scheduler;
this.deps = []; this.active = true; this.parent = null;
}
run() {
if (!this.active) return this.fn();
this.parent = activeEffect;
shouldTrack = true;
activeEffect = this;
this.deps.forEach(d => d.delete(this));
this.deps.length = 0;
try { return this.fn(); }
finally { activeEffect = this.parent; shouldTrack = false; this.parent = null; }
}
stop() { if (this.active) { this.deps.forEach(d => d.delete(this)); this.active = false; } }
}
// ============ reactive ============
const baseHandlers = {
get(t, k, r) {
if (k === RAW) return t;
const res = Reflect.get(t, k, r);
track(t, k);
if (res !== null && typeof res === 'object') return reactive(res);
return res;
},
set(t, k, v, r) {
const old = t[k];
const had = Array.isArray(t) ? Number(k) < t.length : Object.prototype.hasOwnProperty.call(t, k);
if (v && v[RAW]) v = v[RAW];
const res = Reflect.set(t, k, v, r);
if (t === r[RAW]) {
if (!had) trigger(t, 'add', k);
else if (v !== old && (old === old || v === v)) trigger(t, 'set', k);
}
return res;
},
deleteProperty(t, k) {
const had = Object.prototype.hasOwnProperty.call(t, k);
const res = Reflect.deleteProperty(t, k);
if (had && res) trigger(t, 'delete', k);
return res;
}
};
function reactive(target) {
if (typeof target !== 'object' || target === null) return target;
if (target[RAW]) return target;
if (reactiveMap.has(target)) return reactiveMap.get(target);
const proxy = new Proxy(target, baseHandlers);
reactiveMap.set(target, proxy);
return proxy;
}
// ============ ref ============
class RefImpl {
constructor(v) { this._raw = v; this._val = typeof v === 'object' && v ? reactive(v) : v; this.dep = new Set(); this.__v_isRef = true; }
get value() { if (shouldTrack && activeEffect) { this.dep.add(activeEffect); activeEffect.deps.push(this.dep); } return this._val; }
set value(n) { if (n !== this._raw) { this._raw = n; this._val = typeof n === 'object' && n ? reactive(n) : n; this.dep.forEach(e => e !== activeEffect && (e.scheduler ? e.scheduler() : e.run())); } }
}
function ref(v) { return v && v.__v_isRef ? v : new RefImpl(v); }
// ============ effect ============
function effect(fn, opts = {}) {
const e = new ReactiveEffect(fn, opts.scheduler);
if (!opts.lazy) e.run();
const runner = e.run.bind(e); runner.effect = e; return runner;
}
// ============ computed ============
function computed(getter) {
let val, dirty = true;
const dep = new Set();
const _eff = new ReactiveEffect(getter, () => { if (!dirty) { dirty = true; dep.forEach(e => e !== activeEffect && (e.scheduler ? e.scheduler() : e.run())); } });
return {
__v_isRef: true,
get value() { if (shouldTrack && activeEffect) { dep.add(activeEffect); activeEffect.deps.push(dep); } if (dirty) { dirty = false; val = _eff.run(); } return val; },
set value(v) { console.warn('computed is readonly'); }
};
}
// ============ watch ============
function watch(source, cb, opts = {}) {
const getter = typeof source === 'function' ? source : () => source.value;
let old, cleanup;
const onCleanup = fn => { cleanup = fn; };
const job = () => { if (cleanup) cleanup(); const nv = _eff.run(); cb(nv, old, onCleanup); old = nv; };
const _eff = new ReactiveEffect(getter, job);
if (opts.immediate) { job(); } else { old = _eff.run(); }
}
五、测试验证
5.1 基础响应式
const state = reactive({ count: 0 });
effect(() => {
console.log('count:', state.count);
});
// 输出: count: 0
state.count = 1;
// 输出: count: 1
state.count = 2;
// 输出: count: 2
5.2 嵌套对象
const state = reactive({
user: { name: 'Vue', age: 3 }
});
effect(() => {
console.log('name:', state.user.name);
});
// 输出: name: Vue
state.user.name = 'Vue 4';
// 输出: name: Vue 4
5.3 数组操作
const list = reactive([1, 2, 3]);
effect(() => {
console.log('length:', list.length);
});
// 输出: length: 3
list.push(4);
// 输出: length: 4
list[0] = 99;
// 数组索引修改也能触发
5.4 computed 缓存
const state = reactive({ count: 1 });
let callCount = 0;
const double = computed(() => {
callCount++;
return state.count * 2;
});
console.log(double.value); // 2, callCount = 1
console.log(double.value); // 2, callCount = 1 (缓存命中)
state.count = 5;
console.log(double.value); // 10, callCount = 2
console.log(double.value); // 10, callCount = 2 (再次缓存)
5.5 分支切换
const state = reactive({ flag: true, a: 1, b: 2 });
effect(() => {
console.log('result:', state.flag ? state.a : state.b);
});
// 输出: result: 1
state.b = 99;
// 无输出(不依赖 b)
state.flag = false;
// 输出: result: 2
state.a = 100;
// 无输出(切换后不再依赖 a)
5.6 watch
const state = reactive({ name: 'Vue' });
watch(
() => state.name,
(newVal, oldVal) => {
console.log(`${oldVal} -> ${newVal}`);
}
);
state.name = 'React';
// 输出: Vue -> React
state.name = 'Solid';
// 输出: React -> Solid
六、与 Vue 3 源码的对比
| 特性 | 我们的实现 | Vue 3 源码 |
|---|---|---|
| 依赖存储 | WeakMap → Map → Set | 相同 |
| effect 栈 | parent 指针链表 | 相同 |
| 依赖清理 | effect 执行前清除 | 相同 |
| 触发方式 | 创建副本遍历 | toReactive + 调度器 |
| 数组支持 | 基本支持 | 完整(重写方法 + length 追踪) |
| 集合支持 | ❌ | ✅ collectionHandlers |
| 性能优化 | 无 | effectScope、bitmask tracks |
| 调度 | 直接 run | queueJob 微任务合并 |
| 防递归 | effect !== activeEffect |
相同 |
我们的实现省略了一些工程细节,但核心机制与 Vue 3 一致。Vue 3 额外做了:
- 微任务调度:将多次状态变更合并为一次更新
- effectScope:批量管理 effect 生命周期
- 集合处理器:完整支持 Map/Set/WeakMap/WeakSet
- 性能优化:使用 bitwise tracks 减少不必要的依赖收集
- 类型系统:完整的 TypeScript 类型推导
七、总结
通过约 200 行代码,我们实现了一个包含 reactive、ref、effect、computed、watch 的完整响应式系统。核心机制包括:
- Proxy 代理 通过拦截 get/set 实现
reactive - WeakMap → Map → Set 三层结构管理依赖关系
- track/trigger 负责在 get 时收集依赖,set 时派发更新
- parent 指针 实现 effect 嵌套
- cleanup 机制 解决分支切换时的冗余依赖
- dirty flag 实现 computed 的惰性计算与缓存
- scheduler 实现 watch 与组件更新的调度
理解这些核心原理后,再去看 Vue 3 的 reactivity 源码 就会豁然开朗。
Comments | 0条评论