深入 Vue3 响应式原理:从 Proxy 到手写 ref/reactive

本文将带你探讨 Proxy、ref/reactive、响应式、双向绑定之间的差别,然后一个一个认识它们,最后带你手写一个简化版的 Vue3 响应式系统,完整实现 reactiverefeffect、依赖追踪和更新触发。

本文默认你已经使用过 Vue3 中的 ref/reactive API。

一、引言

Vue 3 的响应式系统经过彻底重构,采用 ES6 的 Proxy 替代了 Vue 2 的 Object.defineProperty,这一变革带来了三大优势:

  1. 更强大的响应能力:支持动态属性增删、数组索引修改等场景
  2. 更高的性能:惰性监听和更精确的依赖追踪
  3. 更完善的数据结构支持:原生支持 Map、Set 等集合类型

理解这套机制不仅能提升开发效率,更是掌握 Vue 核心设计思想的关键。

二、核心概念关系

  • Proxy:ES6 中的代理,拦截对象的操作,用于实现响应式
  • 响应式:依赖收集(track)和更新触发(trigger),数据变化时更新相关数据
  • ref/reactive:暴露给开发者的响应式 API
  • 双向绑定:v-model

他们的关系是

graph TD
    A[Proxy 代理层] --> B[响应式系统]
    B --> C[reactive/ref API]
    C --> D[双向绑定 v-model]

三、vue2 响应式的原理和缺陷

(了解 Object.defineProperty 可跳过本章节。)

在 vue2 中使用 Object.defineProperty 来实现响应式,可以拦截数据读取和赋值操作。

注意:下面代码实现一个最简单的数据劫持,仅仅是数据劫持。

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
function defineReactive(obj, key) {
let value = obj[key]
Object.defineProperty(obj, key, {
get() {
console.log('读取:', key)
return value
},
set(newVal) {
console.log('更新:', key, newVal)
value = newVal
}
})
}

// 对象
let obj = {
name: 'zs',
age: 18,
isMale: true
}

// 遍历劫持
Object.keys(obj).forEach(key => defineReactive(obj, key))


if (obj.isMale) {
console.log('是男性')
}

obj.name = 'ls'
const age = obj.age

// 执行结果:
// 读取: isMale
// 是男性
// 更新: name ls
// 读取: age

vue2 的这种实现方式会带来一些问题:

  • 不能监听对象属性的新增/删除
  • 数组 API 以及下标操作无法监听
  • 深层监听,造成性能问题

四、Proxy 的出现

(了解 Proxy 可跳过本章节。)

Vue3 中使用 Proxy 重构响应式原理,就可以解决上面的问题。

用法

Proxy(target, handler) 是一个构造函数,创建一个对象的代理,可以拦截对代理的基本操作。

  • target:要拦截的目标对象
  • handler:一个对象,定义了各种操作代理

什么是代理呢?可以简单理解为再操作这个代理之前设置一个拦截,当被访问、更新时,都要经过这层拦截,那么开发者就可以在这层拦截中进行各种各样的操作。

handler 可以拦截的操作有:getsethasdeletePropertyownKeysgetOwnPropertyDescriptordefinePropertypreventExtensionsgetPrototypeOfisExtensiblesetPrototypeOfapplyconstruct

演示

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
// 目标对象
const obj = {
name: 'zs',
age: 18
}

// 代理目标对象的 get、set 操作
const p_obj = new Proxy(obj, {
get(target, propKey) {
console.log('读取:', propKey)
// return target[propKey]
return Reflect.get(target, propKey)

},
set(target, propKey, newVal) {
console.log('更新:', propKey, newVal)
// target[propKey] = newVal
Reflect.set(target, propKey, newVal)
}
})

// 操作代理对象的name
p_obj.name = 'ls'
const age = p_obj.age

// 查看目标对象的 name 属性
console.log('obj.name: ', obj.name)

// !!!新增属性!!!
obj.isMale = true

if (p_obj.isMale) {
console.log('是男性')
}

// 执行结果
// 更新: name ls
// 读取: age
// obj.name: ls
// 读取: isMale
// 是男性

可以看到,新增的 isMale 属性也是具有响应式的。

Reflect

通过上面的代码可以看到,我们不是直接操作 target 对象的,而是通过 Reflect API 去操作。

ES6 新推出的 Proxy API 的同时,同时也推出了 Reflect,基本上 Proxy 有的代理行为,Reflect 都有对应的静态方法。

至于为什么要使用 Relect ,有三点。

  1. 正确的 this 绑定
  2. 与 Proxy 方法的对称性
  3. 操作失败时的合理返回值

五、实现 ref/reactive

4.1 effect 和依赖收集

响应式的本质是“依赖追踪 + 变更通知”。当我们访问一个响应式数据时,Vue 会记录这个“依赖”,当数据发生变化时,会通知相关依赖重新执行。这就引出了一个关键函数---- effect

1
2
3
4
5
6
7
let activeEffect = null

function effect(fn) {
activeEffect = fn
fn() // 立即执行一次,触发依赖收集
activeEffect = null // 执行完成后重置
}

当我们调用 effect(fn) 时,Vue 就记录下了正在执行的副作用函数,并在之后数据变动时重新执行它。

4.2 实现 reactive

使用 Proxy 来包裹对象,拦截它的 getset 操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function reactive(target) {
return new Proxy(target, {
// 拦截属性读取
get(target, key, receiver) {
// 反射获取原始值
const res = Reflect.get(target, key, receiver)
track(target, key) // 收集依赖
return res
},

// 拦截属性设置
set(target, key, value, receiver) {
// 反射设置值
const result = Reflect.set(target, key, value, receiver)
trigger(target, key) // 触发更新
return result
}
})
}

访问数据时通过 track 函数收集依赖,当更新数据时通过 trigger 去一个个通知。

实现 ref

reactive 只能处理对象,而 ref 用于处理基本类型(如 numberstring)。它将基本类型包装为一个带 .value 的对象。

同样的,ref 也是跟 reactive 一样的思路:收集依赖、触发依赖。

reactive 不一样的是,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function ref(value) {
return {
// ref 标识
__is_ref: true,

// value 的 getter
get value() {
// 收集依赖
track(this, 'value')
return value
},

// value 的 setter
set value(newVal) {
// 只有值变化时才触发更新
if (value !== newVal) {
value = newVal
// 触发更新
trigger(this, 'value')
}
}
}
}

如何收集依赖、触发更新,track/trigger

为了追踪依赖并在变化时通知更新,我们使用 WeakMap → Map → Set 的结构:

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
/**
* 全局依赖存储
* 结构: WeakMap<target, Map<key, Set<effect>>>
*/
// 第一级:目标对象 → 依赖映射
// 第二级:属性键 → 依赖集合
// 第三级:Set 存储 effect
const targetMap = new WeakMap()

function track(target, key) {
// 没有活跃的 effect 则直接返回
if (!activeEffect) return

// 获取 target 对应的依赖映射
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}

// 获取 key 对应的依赖集合
let dep = depsMap.get(key)
if (!dep) {
dep = new Set()
depsMap.set(key, dep)
}

// 将当前 effect 添加到依赖集合
dep.add(activeEffect)
}

function trigger(target, key) {
// 获取 target 对应的所有依赖
const depsMap = targetMap.get(target)
if (!depsMap) return

// 获取 key 对应的所有 effect
const dep = depsMap.get(key)
if (dep) {
// 执行所有关联的 effect
dep.forEach(effect => effect())
}
}

这种依赖收集的结构是响应式系统的核心,它允许我们精确地追踪某个 key 被哪些 effect 使用了。

六、总结

Vue3 响应式工作流

sequenceDiagram
    participant Component as 组件
    participant Proxy as Proxy拦截器
    participant Deps as 依赖系统
    
    Component->>Proxy: 读取数据 (get)
    activate Proxy
    Note right of Proxy: 触发get拦截
    Proxy->>Deps: track(target, key)
    Note left of Deps: 存储activeEffect
到WeakMap→Map→Set deactivate Proxy Component->>Proxy: 修改数据 (set) activate Proxy Note right of Proxy: 触发set拦截 Proxy->>Deps: trigger(target, key) Deps->>Component: 通知关联effect执行 deactivate Proxy

完整代码

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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
/**
* 全局依赖存储
* 结构: WeakMap<target, Map<key, Set<effect>>>
*/
// 第一级:目标对象 → 依赖映射
// 第二级:属性键 → 依赖集合
// 第三级:Set 存储 effect
const targetMap = new WeakMap()

// 当前正在执行的 effect 函数
let activeEffect = null

/**
* 注册副作用函数
* @param {Function} fn - 需要响应式执行的函数
*/
function effect(fn) {
// 设置当前活跃的 effect
activeEffect = fn
// 立即执行一次,触发依赖收集
fn()
// 执行完成后重置
activeEffect = null
}

/**
* 收集依赖
* @param {Object} target - 目标对象
* @param {string|symbol} key - 属性键
*/
function track(target, key) {
// 没有活跃的 effect 则直接返回
if (!activeEffect) return

// 获取 target 对应的依赖映射
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}

// 获取 key 对应的依赖集合
let dep = depsMap.get(key)
if (!dep) {
dep = new Set()
depsMap.set(key, dep)
}

// 将当前 effect 添加到依赖集合
dep.add(activeEffect)
}

/**
* 触发更新
* @param {Object} target - 目标对象
* @param {string|symbol} key - 属性键
*/
function trigger(target, key) {
// 获取 target 对应的所有依赖
const depsMap = targetMap.get(target)
if (!depsMap) return

// 获取 key 对应的所有 effect
const dep = depsMap.get(key)
if (dep) {
// 执行所有关联的 effect
dep.forEach(effect => effect())
}
}

/**
* 创建响应式对象
* @param {Object} target - 目标对象
* @returns {Proxy} 响应式代理
*/
function reactive(target) {
return new Proxy(target, {
// 拦截属性读取
get(target, key, receiver) {
// 反射获取原始值
const res = Reflect.get(target, key, receiver)
// 收集依赖
track(target, key)
return res
},

// 拦截属性设置
set(target, key, value, receiver) {
// 反射设置值
const result = Reflect.set(target, key, value, receiver)
// 触发更新
trigger(target, key)
return result
}
})
}

/**
* 创建响应式引用
* @param {*} value - 初始值
* @returns {Object} 响应式引用对象
*/
function ref(value) {
return {
// ref 标识
__is_ref: true,

// value 的 getter
get value() {
// 收集依赖
track(this, 'value')
return value
},

// value 的 setter
set value(newVal) {
// 只有值变化时才触发更新
if (value !== newVal) {
value = newVal
// 触发更新
trigger(this, 'value')
}
}
}
}

测试:

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

// ===================== 测试案例 =====================

// 测试1: reactive 基本功能
console.log('===== reactive测试 =====')
const person = reactive({
name: '张三',
age: 25
})

effect(() => {
console.log(`个人信息: ${person.name}, ${person.age}岁`)
})

person.name = '李四' // 触发 effect
person.age = 30 // 触发 effect

// 测试2: ref 基本功能
console.log('\n===== ref测试 =====')
const count = ref(0)

effect(() => {
console.log(`当前计数: ${count.value}`)
})

count.value++ // 触发 effect
count.value++ // 再次触发

// 测试3: ref 与 reactive 结合
console.log('\n===== 结合测试 =====')
const state = reactive({
id: 1,
score: ref(80)
})

effect(() => {
console.log(`学生信息: ID=${state.id}, 分数=${state.score.value}`)
})

state.id = 2 // 触发 effect
state.score.value = 90 // 触发 effect

测试输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
===== reactive测试 =====
个人信息: 张三, 25岁
个人信息: 李四, 25岁
个人信息: 李四, 30岁

===== ref测试 =====
当前计数: 0
当前计数: 1
当前计数: 2

===== 结合测试 =====
学生信息: ID=1, 分数=80
学生信息: ID=2, 分数=80
学生信息: ID=2, 分数=90

参考