本文将带你探讨 Proxy、ref/reactive
、响应式、双向绑定之间的差别,然后一个一个认识它们,最后带你手写一个简化版的 Vue3 响应式系统,完整实现 reactive
、ref
、effect
、依赖追踪和更新触发。
本文默认你已经使用过 Vue3 中的 ref/reactive
API。
一、引言
Vue 3 的响应式系统经过彻底重构,采用 ES6 的 Proxy
替代了 Vue 2 的 Object.defineProperty
,这一变革带来了三大优势:
- 更强大的响应能力:支持动态属性增删、数组索引修改等场景
- 更高的性能:惰性监听和更精确的依赖追踪
- 更完善的数据结构支持:原生支持 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
|
vue2 的这种实现方式会带来一些问题:
- 不能监听对象属性的新增/删除
- 数组 API 以及下标操作无法监听
- 深层监听,造成性能问题
四、Proxy 的出现
(了解 Proxy 可跳过本章节。)
Vue3 中使用 Proxy 重构响应式原理,就可以解决上面的问题。
用法
Proxy(target, handler)
是一个构造函数,创建一个对象的代理,可以拦截对代理的基本操作。
target
:要拦截的目标对象
handler
:一个对象,定义了各种操作代理
什么是代理呢?可以简单理解为再操作这个代理之前设置一个拦截,当被访问、更新时,都要经过这层拦截,那么开发者就可以在这层拦截中进行各种各样的操作。
handler
可以拦截的操作有:get
、set
、has
、deleteProperty
、ownKeys
、getOwnPropertyDescriptor
、defineProperty
、preventExtensions
、getPrototypeOf
、isExtensible
、setPrototypeOf
、apply
、construct
演示
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 }
const p_obj = new Proxy(obj, { get(target, propKey) { console.log('读取:', propKey) return Reflect.get(target, propKey)
}, set(target, propKey, newVal) { console.log('更新:', propKey, newVal) Reflect.set(target, propKey, newVal) } })
p_obj.name = 'ls' const age = p_obj.age
console.log('obj.name: ', obj.name)
obj.isMale = true
if (p_obj.isMale) { console.log('是男性') }
|
可以看到,新增的 isMale
属性也是具有响应式的。
Reflect
通过上面的代码可以看到,我们不是直接操作 target
对象的,而是通过 Reflect
API 去操作。
ES6 新推出的 Proxy
API 的同时,同时也推出了 Reflect
,基本上 Proxy
有的代理行为,Reflect
都有对应的静态方法。
至于为什么要使用 Relect
,有三点。
- 正确的
this
绑定
- 与 Proxy 方法的对称性
- 操作失败时的合理返回值
五、实现 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 来包裹对象,拦截它的 get
和 set
操作。
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
用于处理基本类型(如 number
、string
)。它将基本类型包装为一个带 .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 { __is_ref: true,
get value() { track(this, 'value') return value },
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
|
const targetMap = new WeakMap()
function track(target, key) { if (!activeEffect) return
let depsMap = targetMap.get(target) if (!depsMap) { depsMap = new Map() targetMap.set(target, depsMap) }
let dep = depsMap.get(key) if (!dep) { dep = new Set() depsMap.set(key, dep) }
dep.add(activeEffect) }
function trigger(target, key) { const depsMap = targetMap.get(target) if (!depsMap) return
const dep = depsMap.get(key) if (dep) { 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
|
const targetMap = new WeakMap()
let activeEffect = null
function effect(fn) { activeEffect = fn fn() activeEffect = null }
function track(target, key) { if (!activeEffect) return
let depsMap = targetMap.get(target) if (!depsMap) { depsMap = new Map() targetMap.set(target, depsMap) }
let dep = depsMap.get(key) if (!dep) { dep = new Set() depsMap.set(key, dep) }
dep.add(activeEffect) }
function trigger(target, key) { const depsMap = targetMap.get(target) if (!depsMap) return
const dep = depsMap.get(key) if (dep) { dep.forEach(effect => effect()) } }
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 } }) }
function ref(value) { return { __is_ref: true,
get value() { track(this, 'value') return value },
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
|
console.log('===== reactive测试 =====') const person = reactive({ name: '张三', age: 25 })
effect(() => { console.log(`个人信息: ${person.name}, ${person.age}岁`) })
person.name = '李四' person.age = 30
console.log('\n===== ref测试 =====') const count = ref(0)
effect(() => { console.log(`当前计数: ${count.value}`) })
count.value++ count.value++
console.log('\n===== 结合测试 =====') const state = reactive({ id: 1, score: ref(80) })
effect(() => { console.log(`学生信息: ID=${state.id}, 分数=${state.score.value}`) })
state.id = 2 state.score.value = 90
|
测试输出:
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
|
参考