本文将带你探讨 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
他们的关系是
Proxy 代理层 响应式系统 reactive/ref API 双向绑定 v-model
三、vue2 响应式的原理和缺陷
(了解 Object.defineProperty 可跳过本章节。)
在 vue2 中使用 Object.defineProperty
来实现响应式,可以拦截数据读取和赋值操作。
注意:下面代码实现一个最简单的数据劫持,仅仅是数据劫持。
javascript
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
可以拦截的操作有:get
、set
、has
、deleteProperty
、ownKeys
、getOwnPropertyDescriptor
、defineProperty
、preventExtensions
、getPrototypeOf
、isExtensible
、setPrototypeOf
、apply
、construct
演示
javascript
// 目标对象
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
,有三点。
- 正确的
this
绑定 - 与 Proxy 方法的对称性
- 操作失败时的合理返回值
五、实现 ref/reactive
4.1 effect 和依赖收集
响应式的本质是"依赖追踪 + 变更通知"。当我们访问一个响应式数据时,Vue 会记录这个"依赖",当数据发生变化时,会通知相关依赖重新执行。这就引出了一个关键函数---- effect
。
javascript
let activeEffect = null
function effect(fn) {
activeEffect = fn
fn() // 立即执行一次,触发依赖收集
activeEffect = null // 执行完成后重置
}
当我们调用 effect(fn)
时,Vue 就记录下了正在执行的副作用函数,并在之后数据变动时重新执行它。
4.2 实现 reactive
使用 Proxy 来包裹对象,拦截它的 get
和 set
操作。
javascript
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
不一样的是,
javascript
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
的结构:
javascript
/**
* 全局依赖存储
* 结构: 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 响应式工作流
组件 Proxy拦截器 依赖系统 读取数据 (get) 触发get拦截 track(target, key) 存储activeEffect 到WeakMap→Map→Set 修改数据 (set) 触发set拦截 trigger(target, key) 通知关联effect执行 组件 Proxy拦截器 依赖系统
完整代码
javascript
/**
* 全局依赖存储
* 结构: 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')
}
}
}
}
测试:
javascript
// ===================== 测试案例 =====================
// 测试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
测试输出:
===== reactive测试 =====
个人信息: 张三, 25岁
个人信息: 李四, 25岁
个人信息: 李四, 30岁
===== ref测试 =====
当前计数: 0
当前计数: 1
当前计数: 2
===== 结合测试 =====
学生信息: ID=1, 分数=80
学生信息: ID=2, 分数=80
学生信息: ID=2, 分数=90
参考
-
面试官:Vue3.0里为什么要用 Proxy API 替代 defineProperty API ? | web前端面试 - 面试官系列
-
手写简单vue3响应式原理在之前的文章里小浪介绍过Vue2的响应式原理,评论中有掘友评论想让我介绍Vue3的响应式原理, - 掘金
-
chatGPT / deepseek
首发地址:https://blog.xchive.top/2025/deep-dive-into-vue3-reactivity-from-proxy-to-hand-rolling.html