【Vue】响应式原理

简易代码实现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(); // 使用 Set 来存储依赖函数,避免重复添加依赖函数
}

addDepend() {
// 避免添加空的函数
if (activeReactiveFn) {
this.reactiveFns.add(activeReactiveFn);
}
}

notify() {
this.reactiveFns.forEach((fn) => fn());
}
}

/**
* 获取依赖对象depend
* @param {*} target 目标对象
* @param {*} key 依赖键
* @return {Depend} 依赖对象
*/
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;
}

/**
* 添加依赖函数
* @param {Function} fn - 要依赖的函数
*/
function watchFn(fn) {
activeReactiveFn = fn;
fn();
activeReactiveFn = null;
}

/**
* 创建一个响应式对象
* @param {Object} obj - 要转换为响应式的对象
* @return {Proxy} 响应式对象
*/
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();
// 结构: targetMap
// ├─ target1 (WeakMap key)
// │ └─ propertyMap (Map)
// │ ├─ 'name' → Depend { reactiveFns: Set(...) }
// │ └─ 'age' → Depend { reactiveFns: Set(...) }
// └─ target2 (WeakMap key)
// └─ propertyMap (Map)
// └─ ...

为什么使用 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;
}

执行流程:

  1. targetMap 中获取该对象的属性映射表
  2. 如果不存在,则创建一个新的 Map
  3. 从属性映射表中获取该属性的依赖对象
  4. 如果不存在,则创建一个新的 Depend 对象
  5. 返回依赖对象

🔄 响应式代理实现

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(); // 执行函数(此时会触发 get 操作)
activeReactiveFn = null; // 清除标记
}

工作流程:

  1. 设置全局的 activeReactiveFn 变量为当前函数
  2. 执行函数体
  3. 函数执行期间,所有访问的响应式属性都会收集这个函数作为依赖
  4. 函数执行完毕后,清除 activeReactiveFn 标记

📊 完整流程演示

场景:创建响应式对象并监听

1
2
3
4
5
6
7
8
9
10
11
12
13
// 1. 创建普通对象
const data = {
name: "张三",
age: 20,
};

// 2. 转换为响应式对象
const dataProxy = reactive(data);

// 3. 监听函数 - 访问 name 属性
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
dataProxy.name = "李四";

此时发生了什么?

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)

通过响应式系统,表单输入会实时更新数据,数据变化也会实时更新表单。


⚠️ 局限性

这个简化实现有以下局限:

  1. 不支持嵌套对象响应式:只有第一层属性是响应式的
  2. 不支持数组方法:数组的 pushpop 等方法无法触发响应
  3. 性能考虑:每次访问都会触发 get,可能产生性能开销
  4. 无法检测属性添加/删除:只能追踪已有属性的变化

Vue 3+ 使用 Proxy 解决了这些问题,而 Vue 2 则使用 Object.defineProperty 递归处理对象的每个属性。


🎓 学习要点

通过这个简化的实现,我们可以理解:

  1. ✅ Vue 如何追踪数据的访问和修改
  2. ✅ 依赖收集的时机和方式
  3. ✅ 为什么数据修改后能自动更新视图
  4. ✅ WeakMap、Map、Set 这些数据结构的实际应用
  5. ✅ Proxy 和 Reflect API 在响应式系统中的作用

📚 相关资源