简易代码实现Vue的响应式原理
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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
| let activeReactiveFn = null; const targetMap = new WeakMap();
class Depend { constructor() { this.reactiveFns = new Set(); }
addDepend() { if (activeReactiveFn) { this.reactiveFns.add(activeReactiveFn); } }
notify() { this.reactiveFns.forEach((fn) => fn()); } }
function getDepend(target, key) { let map = targetMap.get(target); if (!map) { map = new Map(); targetMap.set(target, map); } let depend = map.get(key); if (!depend) { depend = new Depend(); map.set(key, depend); } return depend; }
function watchFn(fn) { activeReactiveFn = fn; fn(); activeReactiveFn = null; }
function reactive(obj) { return new Proxy(obj, { get(target, key, receiver) { getDepend(target, key).addDepend(); return Reflect.get(target, key, receiver); },
set(target, key, newValue, receiver) { Reflect.set(target, key, newValue, receiver); getDepend(target, key).notify(); return true; }, }); }
const data = { name: "张三", age: 20, };
const dataProxy = reactive(data);
watchFn(function () { console.log("访问 name", dataProxy.name); });
watchFn(function () { console.log("访问 age", dataProxy.age); });
console.log("-----------------------------------------");
dataProxy.age = 21; dataProxy.name = "段誉";
|
Vue 响应式原理详解
📌 概述
响应式系统是 Vue 的灵魂,它使得当数据变化时,视图能够自动更新。
🎯 核心概念
1. 依赖收集 (Dependency Collection)
依赖收集是响应式系统的第一步。当组件渲染或执行一个观察函数时,需要记录它访问了哪些数据属性。
核心实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class Depend { constructor() { this.reactiveFns = new Set(); }
addDepend() { if (activeReactiveFn) { this.reactiveFns.add(activeReactiveFn); } }
notify() { this.reactiveFns.forEach((fn) => fn()); } }
|
工作原理:
- 每个数据属性都对应一个
Depend 对象 Depend 对象维护一个 Set 集合,存储所有依赖该属性的响应函数- 当该属性被访问时,当前正在执行的函数会被添加到这个集合中
- 当该属性被修改时,集合中的所有函数都会被重新执行
🔗 依赖映射关系
2. TargetMap(目标映射表)
为了建立 对象 → 属性 → 依赖函数 的对应关系,使用了嵌套的数据结构:
1 2 3 4 5 6 7 8 9
| const targetMap = new WeakMap();
|
为什么使用 WeakMap?
- 当对象被垃圾回收时,对应的映射关系会自动清理,避免内存泄漏
- 相比普通 Map,WeakMap 的键只能是对象,且键的引用是弱引用
3. getDepend 函数
这个函数获取或创建指定对象属性对应的依赖对象:
1 2 3 4 5 6 7 8 9 10 11 12 13
| function getDepend(target, key) { let map = targetMap.get(target); if (!map) { map = new Map(); targetMap.set(target, map); } let depend = map.get(key); if (!depend) { depend = new Depend(); map.set(key, depend); } return depend; }
|
执行流程:
- 从
targetMap 中获取该对象的属性映射表 - 如果不存在,则创建一个新的 Map
- 从属性映射表中获取该属性的依赖对象
- 如果不存在,则创建一个新的 Depend 对象
- 返回依赖对象
🔄 响应式代理实现
4. Reactive 函数 - 核心代理
通过 Proxy 拦截对象的读写操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| function reactive(obj) { return new Proxy(obj, { get(target, key, receiver) { getDepend(target, key).addDepend(); return Reflect.get(target, key, receiver); },
set(target, key, newValue, receiver) { Reflect.set(target, key, newValue, receiver); getDepend(target, key).notify(); return true; }, }); }
|
Get 拦截器(读操作):
- 当访问属性时触发
- 调用
addDepend() 收集当前的响应函数为依赖 - 返回属性值
Set 拦截器(写操作):
- 当修改属性时触发
- 先执行属性更新
- 再调用
notify() 通知所有依赖该属性的函数重新执行
👁️ 监听函数
5. watchFn 函数
这是连接响应系统的纽带,负责执行函数并收集其依赖:
1 2 3 4 5
| function watchFn(fn) { activeReactiveFn = fn; fn(); activeReactiveFn = null; }
|
工作流程:
- 设置全局的
activeReactiveFn 变量为当前函数 - 执行函数体
- 函数执行期间,所有访问的响应式属性都会收集这个函数作为依赖
- 函数执行完毕后,清除
activeReactiveFn 标记
📊 完整流程演示
场景:创建响应式对象并监听
1 2 3 4 5 6 7 8 9 10 11 12 13
| const data = { name: "张三", age: 20, };
const dataProxy = reactive(data);
watchFn(function () { console.log("访问 name", dataProxy.name); });
|
此时发生了什么?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| watchFn 执行流程: │ ├─ activeReactiveFn = function() { console.log("访问 name", dataProxy.name); } │ ├─ 执行函数体 │ │ │ └─ dataProxy.name 触发 get 拦截器 │ │ │ ├─ getDepend(data, 'name') 获取 Depend 对象 │ ├─ depend.addDepend() │ │ └─ 将当前函数添加到 Depend.reactiveFns 集合中 │ │ │ └─ 返回 "张三" │ └─ activeReactiveFn = null
|
场景:修改响应式属性
此时发生了什么?
1 2 3 4 5 6 7 8 9 10 11 12 13
| set 拦截器执行流程: │ ├─ Reflect.set(data, 'name', "李四") │ └─ data.name 更新为 "李四" │ ├─ getDepend(data, 'name') 获取 Depend 对象 ├─ depend.notify() │ │ │ └─ 遍历所有依赖函数 │ └─ 执行: console.log("访问 name", "李四") │ 输出: 访问 name 李四 │ └─ 返回 true
|
🔑 关键要点总结
三大核心机制
| 机制 | 说明 | 实现方式 |
|---|
| 依赖收集 | 记录哪些函数依赖某个属性 | 在 get 拦截器中执行 addDepend() |
| 依赖存储 | 维护对象属性与依赖函数的映射关系 | 使用 WeakMap + Map + Set 的嵌套结构 |
| 依赖触发 | 当数据修改时,自动执行所有依赖函数 | 在 set 拦截器中执行 notify() |
数据结构关系
1 2 3 4 5 6 7 8 9 10
| weakMap: targetMap ↓ ├─ obj (弱引用) │ ↓ │ map: {name → depend1, age → depend2, ...} │ ↓ │ ├─ depend1: {reactiveFns: Set[fn1, fn2, ...]} │ └─ depend2: {reactiveFns: Set[fn3, fn4, ...]} │ └─ ...
|
💡 应用场景
1. 自动更新UI
当数据变化时,Vue 会自动重新渲染组件,这正是通过这个响应式系统实现的。
2. 计算属性 (Computed)
计算属性会自动追踪其依赖的数据,当依赖数据变化时自动重新计算。
3. 侦听器 (Watch)
监听函数会在所监听的数据变化时执行回调。
4. 双向数据绑定 (v-model)
通过响应式系统,表单输入会实时更新数据,数据变化也会实时更新表单。
⚠️ 局限性
这个简化实现有以下局限:
- 不支持嵌套对象响应式:只有第一层属性是响应式的
- 不支持数组方法:数组的
push、pop 等方法无法触发响应 - 性能考虑:每次访问都会触发
get,可能产生性能开销 - 无法检测属性添加/删除:只能追踪已有属性的变化
Vue 3+ 使用 Proxy 解决了这些问题,而 Vue 2 则使用 Object.defineProperty 递归处理对象的每个属性。
🎓 学习要点
通过这个简化的实现,我们可以理解:
- ✅ Vue 如何追踪数据的访问和修改
- ✅ 依赖收集的时机和方式
- ✅ 为什么数据修改后能自动更新视图
- ✅ WeakMap、Map、Set 这些数据结构的实际应用
- ✅ Proxy 和 Reflect API 在响应式系统中的作用
📚 相关资源