手写简易 Vue3 响应式系统
手写简易 Vue3 响应式系统
一句话概括
手写 Vue 3 响应式系统的三个核心模块:reactive(Proxy 代理)、effect(副作用追踪)、computed(惰性求值),加起来不到 80 行代码,却是理解整个框架的钥匙。
核心知识点
1. reactive——代理 + 依赖收集 + 触发更新
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
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); // 触发更新
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——自动追踪
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();
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——惰性求值
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');
if (dirty) { dirty = false; value = _effect(); }
return value;
}
};
return _result;
}
4. ref——原始值包装
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function ref(value) {
return {
get value() {
track(this, 'value');
return value;
},
set value(newVal) {
if (newVal !== value) {
value = newVal;
trigger(this, 'value');
}
}
};
}
5. watch——精确监听
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();
cb(newValue, oldValue);
oldValue = newValue;
}
});
oldValue = runner(); // 首次运行收集依赖
}
其实你每天都在用
- 模板渲染:
<div>9</div>→ render effect 自动收集count→count++触发 diff 更新 - v-model 双向绑定:输入一个字 → Proxy set → trigger → render effect 重跑 → DOM 更新
- computed 派生:
const full = computed(() => first + last)→ 第一/末名不变就不算 - watch 自动请求:
watch(route.params.id, fetchData)→ id 变了立刻发请求 - 组件卸载清理:
onUnmounted→stop(effect)→ cleanup 清空所有 dep 引用
常见误解
❌ 误区:「手写 reactive 只要拦截 get/set 就够了」
deleteProperty也必须拦截。delete obj.key不触发 set,但 Vue 2 完全做不到拦截,Vue 3 通过deleteProperty支持了。❌ 误区:「computed 的值在依赖变化时自动重新计算」 错!依赖变化只设置
dirty = true,真正计算发生在下次读取.value时。如果依赖变了但没人读,computed 永远不会重算——这就是惰性求值。❌ 误区:「手写版不需要 cleanUp」 绝对需要。不清理旧依赖会导致”幽灵依赖”——
flag从 true 切 false 后,改a仍然触发本不应该关联的 effect。❌ 误区:「ref 和 reactive 可以互相替换」 ref 解决的是 Proxy 只能代理对象的问题——
ref(0)的本质是{ value: 0 }上的 getter/setter,套了层壳。对象级响应式用 reactive(Proxy),原始值用 ref。
一句话总结
80 行代码写出 Vue 3 响应式核心:Proxy 劫持 → track 记账 → trigger 催账 → scheduler 调度——从这里开始,computed / watch / 组件渲染都是这套流水线上的不同工位。
本文由作者按照 CC BY 4.0 进行授权