目标

不造轮子,只拆轮子。

本文带你用 150 行纯 TypeScript 实现 Vue 3 响应式核心 API 的最小可用子集

  • reactive(obj) —— 深度响应式对象
  • ref(val) —— 基础类型包装器
  • effect(fn) —— 副作用函数(自动追踪依赖)
  • computed(getter) —— 缓存派生值
  • watchEffect(fn) —— 立即执行 + 自动追踪

无 Virtual DOM、无编译器、无 Patch,只聚焦「依赖收集 → 触发更新」这条主线。


一、核心数据结构:依赖图

Vue 3 用 WeakMap → Map → Set 三层结构存「谁依赖谁」:

1
2
// target -> key -> Set<effect>
const targetMap = new WeakMap<object, Map<string | symbol, Set<ReactiveEffect>>>();
1
2
3
4
5
6
7
graph LR
A[targetMap: WeakMap] --> B[target: Obj]
B --> C[keyMap: Map]
C --> D[key: 'count']
D --> E[deps: Set<Effect>]
E --> F[effectFn1]
E --> G[effectFn2]

为什么 WeakMap? 键是对象,不阻碍 GC;目标对象被回收时,依赖随之消失,防内存泄漏。


二、effect:副作用收集器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let activeEffect: ReactiveEffect | null = null;

class ReactiveEffect {
public deps: Set<ReactiveEffect>[] = []; // 反向引用:这个 effect 被哪些 key 收集了
constructor(public fn: () => any, public scheduler?: (e: ReactiveEffect) => void) {}
run() {
activeEffect = this;
try { return this.fn(); } finally { activeEffect = null; }
}
}

export function effect(fn: () => any, options?: { scheduler?: (e: ReactiveEffect) => void }) {
const e = new ReactiveEffect(fn, options?.scheduler);
e.run(); // 立即执行一次,完成依赖收集
return e;
}

关键点activeEffect 是全局唯一的「当前正在跑的 effect」。fn() 执行过程中,每次读取响应式属性,都会触发 track,把 activeEffect 存进依赖集合


三、track / trigger:依赖收集与触发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export function track(target: object, key: string | symbol) {
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); // 反向记录,便于 cleanup
}
}

export function trigger(target: object, key: string | symbol) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(key);
if (!dep) return;
// 复制一份再跑,防止 effect 内部修改同一 key 导致死循环
const effects = [...dep];
effects.forEach(e => e.scheduler ? e.scheduler(e) : e.run());
}

四、reactive:Proxy 拦截 get/set

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const reactiveMap = new WeakMap<object, any>();

export function reactive<T extends object>(target: T): T {
if (reactiveMap.has(target)) return reactiveMap.get(target);
const proxy = new Proxy(target, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
track(target, key); // ← 读取时收集依赖
return isObject(res) ? reactive(res) : res; // 深度代理
},
set(target, key, value, receiver) {
const oldVal = (target as any)[key];
const res = Reflect.set(target, key, value, receiver);
if (oldVal !== value) trigger(target, key); // ← 写入时触发更新
return res;
}
});
reactiveMap.set(target, proxy);
return proxy;
}

function isObject(v: any): v is object { return v !== null && typeof v === 'object'; }

深度代理get 里对返回值再次 reactive,实现 obj.nested.prop 任意层级响应。


五、ref:把基本类型包成对象

1
2
3
4
5
6
7
8
9
10
11
12
13
export interface Ref<T> { value: T; __v_isRef: true; }

export function ref<T>(value: T): Ref<T> {
const r: Ref<T> = {
__v_isRef: true,
get value() { track(r, 'value'); return value; },
set value(v) { if (value !== v) { value = v; trigger(r, 'value'); } }
};
return r;
}

// 自动解包:在 reactive 对象里读 ref,直接返回 .value
function unref<T>(r: Ref<T> | T): T { return (r as Ref<T>).__v_isRef ? (r as Ref<T>).value : r; }

六、computed:带缓存的派生值

1
2
3
4
5
6
7
8
9
10
11
12
13
export function computed<T>(getter: () => T) {
let value: T;
let dirty = true;
const effect = new ReactiveEffect(getter, () => { dirty = true; });
return {
__v_isRef: true,
get value() {
if (dirty) { value = effect.run(); dirty = false; }
track(effect, 'value'); // computed 也要被外层 effect 追踪
return value;
}
} as Ref<T>;
}

惰性求值:只有读 .value 时才跑 getter,且缓存结果;依赖变了只标 dirty = true,下次读才重算。


七、watchEffect:语法糖

1
2
3
export function watchEffect(fn: () => any) {
return effect(fn, { scheduler: e => e.run() }); // 同步调度,也可改成微任务队列
}

八、跑个 Demo 验证

1
2
3
4
5
6
7
8
9
10
11
12
// demo.ts
import { reactive, ref, effect, computed, watchEffect } from './reactivity';

const state = reactive({ count: 0, name: 'Vue' });
const double = computed(() => state.count * 2);
const msg = ref('Hello');

effect(() => console.log('effect1:', state.count, double.value));
watchEffect(() => console.log('watchEffect:', msg.value));

setTimeout(() => { state.count++; }, 1000); // 1s 后打印 effect1: 1 2
setTimeout(() => { msg.value = 'World'; }, 2000); // 2s 后打印 watchEffect: World
1
2
3
4
5
npx ts-node demo.ts
# effect1: 0 0
# watchEffect: Hello
# (1s) effect1: 1 2
# (2s) watchEffect: World

九、完整 150 行源码(可直接复制跑)

文件:reactivity.ts

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// reactivity.ts — 150 行极简 Vue 3 响应式核心
const targetMap = new WeakMap<object, Map<string | symbol, Set<ReactiveEffect>>>();
let activeEffect: ReactiveEffect | null = null;

class ReactiveEffect {
deps: Set<ReactiveEffect>[] = [];
constructor(public fn: () => any, public scheduler?: (e: ReactiveEffect) => void) {}
run() { activeEffect = this; try { return this.fn(); } finally { activeEffect = null; } }
}

function track(target: object, key: string | symbol) {
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: object, key: string | symbol) {
const depsMap = targetMap.get(target); if (!depsMap) return;
const dep = depsMap.get(key); if (!dep) return;
[...dep].forEach(e => e.scheduler ? e.scheduler(e) : e.run());
}

function isObject(v: any): v is object { return v !== null && typeof v === 'object'; }
const reactiveMap = new WeakMap<object, any>();

export function reactive<T extends object>(target: T): T {
if (reactiveMap.has(target)) return reactiveMap.get(target);
const proxy = new Proxy(target, {
get(t, k, r) { const res = Reflect.get(t, k, r); track(t, k); return isObject(res) ? reactive(res) : res; },
set(t, k, v, r) { const old = (t as any)[k]; const res = Reflect.set(t, k, v, r); if (old !== v) trigger(t, k); return res; }
});
reactiveMap.set(target, proxy); return proxy;
}

export interface Ref<T> { value: T; __v_isRef: true; }

export function ref<T>(value: T): Ref<T> {
const r: Ref<T> = {
__v_isRef: true,
get value() { track(r, 'value'); return value; },
set value(v) { if (value !== v) { value = v; trigger(r, 'value'); } }
}; return r;
}

export function computed<T>(getter: () => T) {
let val: T; let dirty = true;
const eff = new ReactiveEffect(getter, () => dirty = true);
return { __v_isRef: true, get value() { if (dirty) { val = eff.run(); dirty = false; } track(eff, 'value'); return val; } } as Ref<T>;
}

export function effect(fn: () => any, options?: { scheduler?: (e: ReactiveEffect) => void }) {
const e = new ReactiveEffect(fn, options?.scheduler); e.run(); return e;
}

export function watchEffect(fn: () => any) { return effect(fn, { scheduler: e => e.run() }); }

十、架构图总结

1
2
3
4
5
6
7
8
9
10
11
12
13
flowchart TD
A[用户代码] --> B{effect / computed / watchEffect}
B --> C[activeEffect = 当前 effect]
C --> D[执行 fn 读取响应式数据]
D --> E[Proxy get 拦截]
E --> F[track(target, key)]
F --> G[targetMap 存入 activeEffect]
G --> H[修改数据]
H --> I[Proxy set 拦截]
I --> J[trigger(target, key)]
J --> K[取出 dep Set<Effect>]
K --> L[调度 effect.run / scheduler]
L --> M[视图更新 / 副作用重跑]

十一、还没做的事(留给真实 Vue 做)

特性 本文实现 真实 Vue 3
readonly / shallowReactive
isRef / toRef / toRefs
数组方法拦截(push/splice 等)
effect 停止 / stop / onStop
调度器微任务队列 / flush: 'pre'/'post'/'sync'
customRef / triggerRef
调试工具 onTrack / onTrigger
性能优化(依赖去重、位运算标记)

十二、结语

响应式本质 = 依赖收集 + 触发更新

  • Proxy 拦截读写
  • WeakMap/Map/Set 存依赖图
  • activeEffect 线程局部变量串联「读」与「写」
  • scheduler 决定「何时跑副作用」

把这 150 行吃透,Vue 3 源码 packages/reactivity 就不再是天书。

完整可运行代码:GitHub Gist (自行替换链接)


写于 2026-07-08,TypeScript 5.4 + Node 20 实测通过。
封面/配图均为 picsum.photos 随机生成,实际项目请换用自有 CDN。