proxy_based_reactivity.md (2622B)
1 # Proxy-based Reactivity 2 3 ``` 4 Proxy (trap layer) 5 +------------------+ 6 state.count++ | set trap fires |---> notify subscribers 7 <-- direct --> | get trap fires |---> track dependencies 8 +------------------+ 9 | 10 +------------------+ 11 | Raw state obj | 12 +------------------+ 13 ``` 14 15 ## Reactive State with Proxy 16 17 A JavaScript Proxy wraps your state object and intercepts get and set 18 operations. Instead of calling setState() manually, any direct property 19 assignment automatically triggers subscribers — the same mechanic Vue 3 and MobX 20 use internally. 21 22 ```ts 23 type Subscriber<T> = (state: T) => void; 24 25 function reactive<T extends object>( 26 initial: T, 27 onChange: Subscriber<T>, 28 ): T { 29 return new Proxy(initial, { 30 set(target, prop, value) { 31 (target as any)[prop] = value; 32 onChange(target); 33 return true; 34 }, 35 }); 36 } 37 38 // Usage 39 interface AppState { 40 count: number; 41 user: string | null; 42 } 43 44 const state = reactive<AppState>( 45 { count: 0, user: null }, 46 (s) => console.log("State changed:", s), 47 ); 48 49 state.count++; // logs automatically 50 state.user = "Alice"; // logs automatically 51 ``` 52 53 For finer control, you can track which properties were accessed (get trap) and 54 only notify subscribers that depend on that specific property — this is called 55 dependency tracking: 56 57 ```ts 58 const subscribers = new Map<string | symbol, Set<() => void>>(); 59 let activeEffect: (() => void) | null = null; 60 61 function reactive<T extends object>(initial: T): T { 62 return new Proxy(initial, { 63 get(target, prop) { 64 if (activeEffect) { 65 if (!subscribers.has(prop)) subscribers.set(prop, new Set()); 66 subscribers.get(prop)!.add(activeEffect); 67 } 68 return (target as any)[prop]; 69 }, 70 set(target, prop, value) { 71 (target as any)[prop] = value; 72 subscribers.get(prop)?.forEach((fn) => fn()); 73 return true; 74 }, 75 }); 76 } 77 78 function effect(fn: () => void) { 79 activeEffect = fn; 80 fn(); // run once to collect dependencies via get traps 81 activeEffect = null; 82 } 83 84 // Usage 85 const state = reactive({ count: 0, user: "Alice" }); 86 87 effect(() => { 88 console.log(`Count is: ${state.count}`); // only re-runs when count changes 89 }); 90 91 state.count = 5; // triggers effect 92 state.user = "Bob"; // does NOT trigger effect (not accessed in effect) 93 ``` 94 95 Watch out: Proxy only intercepts the top-level object by default. For deeply 96 nested state you need to recursively wrap nested objects in their own Proxy — 97 which is exactly what Vue 3's reactive() does under the hood.