手写简易 Vue3 响应式系统深度解析
手写 Vue3 响应式的 reactive、effect、computed、ref、watch 五个核心模块加起来不到一百行,却是理解框架的终极钥匙。 面试手写不考记忆力,考的是否理解 WeakMap 三层数据结构的设计意图与 cleanup 清理依赖的必要性。
一句话概括
手写 Vue 3 响应式的五个核心模块——reactive、effect、computed、ref、watch——加起来不到 100 行,却是理解整个框架的终极钥匙。面试手写不考你的记忆力,考的是你是否理解三层数据结构的设计意图和 cleanup 的必要性。
核心知识点
1. reactive——代理 + track + trigger(20 行)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
const targetMap = new WeakMap();
let activeEffect = null;
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
track(target, key);
return (res && typeof res === 'object') ? reactive(res) : res; // 懒递归
},
set(target, key, value, receiver) {
const old = target[key];
const ok = Reflect.set(target, key, value, receiver);
if (old !== value) trigger(target, key); // NaN 需额外处理
return ok;
},
deleteProperty(target, key) { // ← 面试加分:别漏了这个
const had = Object.prototype.hasOwnProperty.call(target, key);
const ok = Reflect.deleteProperty(target, key);
if (had) trigger(target, key);
return ok;
}
});
}
function track(target, key) {
if (!activeEffect) return;
let depsMap = targetMap.get(target);
if (!depsMap) targetMap.set(target, (depsMap = new Map()));
let deps = depsMap.get(key);
if (!deps) depsMap.set(key, (deps = new Set()));
deps.add(activeEffect);
activeEffect.deps.push(deps); // 反向引用
}
function trigger(target, key) {
targetMap.get(target)?.get(key)?.forEach(e => {
if (e !== activeEffect) e.scheduler ? e.scheduler() : e(); // 防自己触发自己
});
}
2. effect——带 cleanup 的自动追踪(15 行)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function effect(fn, options = {}) {
const _effect = () => {
cleanup(_effect); // ① 清理旧依赖
activeEffect = _effect; // ② 上位
fn(); // ③ 执行 → get 时自动 track
activeEffect = null; // ④ 退位
};
_effect.deps = []; // 存反向引用数组
_effect.scheduler = options.scheduler;
_effect();
return _effect;
}
function cleanup(effect) {
effect.deps.forEach(dep => dep.delete(effect));
effect.deps.length = 0;
}
3. computed——dirty 惰性求值(10 行)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function computed(getter) {
let value, dirty = true;
const _effect = effect(getter, {
scheduler() {
if (!dirty) { dirty = true; trigger(_result, 'value'); } // 只设脏 + 通知订阅者
}
});
const _result = {
get value() {
track(_result, 'value'); // computed 自身也是响应式的
if (dirty) { dirty = false; value = _effect(); } // 脏了才算
return value;
}
};
return _result;
}
4. ref——原始值的 Proxy 壳(8 行)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function ref(value) {
const r = {
get value() {
track(r, 'value');
return value;
},
set value(newVal) {
if (newVal !== value) {
value = newVal;
trigger(r, 'value');
}
}
};
return r;
}
为什么需要 ref? Proxy 只能代理对象,ref(0) 本质上创造了一个 { value: 0 } 的 getter/setter 包装。源码里 ref 传对象时内部调用 reactive。
5. watch——带新旧值的调度 effect(10 行)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function watch(source, cb) {
let getter = typeof source === 'function' ? source : () => source.value;
let oldValue;
const runner = effect(getter, {
scheduler() {
const newValue = runner(); // 主动跑 effect 拿最新值
cb(newValue, oldValue); // 新旧值一起给回调
oldValue = newValue; // 更新 oldValue 为下一轮准备
}
});
oldValue = runner(); // 首次执行收集依赖 + 初始化 oldValue
}
其实你每天都在用
- 模板渲染:
<div>{ { count } }</div>→ render effect 自动收集count→count++→ trigger → patch - v-model 双向绑定: 输入 → set 拦截 → trigger → render effect 重跑 → DOM 更新,全链路
- computed 派生:
const full = computed(() => first + last)→ 任一依赖不变就不重算 - watch 自动请求:
watch(route.params.id, fetchData)→ id 变了立即发请求 - 组件卸载:
onUnmounted中stop(effect)→ cleanup 清空所有 dep 引用 → 闭包回收
常见误解(FAQ)
❌ 误区:「手写版不用 cleanup」 不写 cleanup 的 demo 在分支切换场景下是有 bug 的——
effect(() => flag ? a : b)里 flag 从 true 切 false 后,改a仍会触发这个 effect。面试官等的就是你会不会指出这个。❌ 误区:「computed 依赖变了自己会重算」 不会!依赖变只设
dirty = true。真正计算发生在下次.value被读取时。如果 computed 没人用,依赖再变它也永远不会跑——这就是”惰性求值”。❌ 误区:「reactive 拦截 get/set 就行,deleteProperty 不用管」
delete obj.key不触发 set。Vue 2 完全拦截不了,Vue 3 的deletePropertytrap 解决了这个——面试时主动提到是加分项。❌ 误区:「ref 和 reactive 能互相替换」 ref 解决的是 Proxy 只能代理对象的限制。
ref(0)={ value: 0 }的 getter/setter,reactive({ count: 0 })才是 Proxy。对象用 reactive,原始值用 ref——不是可不可替,是能不能用。
一句话总结
100 行写出 Vue 3 响应式:Proxy 接管访问、track 记账、trigger 催账、cleanup 清旧账、scheduler 调度——computed/watch/组件渲染都是这套流水线上不同工位的产品。