目标
不造轮子,只拆轮子。
本文带你用 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 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 >[] = []; 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); } } export function trigger (target : object , key : string | symbol ) { const depsMap = targetMap.get (target); if (!depsMap) return ; const dep = depsMap.get (key); if (!dep) return ; 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; } 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' ); 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 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 ); setTimeout (() => { msg.value = 'World' ; }, 2000 );
九、完整 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 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。