深入Vue3响应式原理:从Proxy到ref-reactive实现
本文将带你探讨 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
他们的关系是
#mermaid-svg-ju3rx5Ly9NLiH0tI {font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-ju3rx5Ly9NLiH0tI .error-icon{fill:#552222;}#mermaid-svg-ju3rx5Ly9NLiH0tI .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ju3rx5Ly9NLiH0tI .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-ju3rx5Ly9NLiH0tI .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ju3rx5Ly9NLiH0tI .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ju3rx5Ly9NLiH0tI .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ju3rx5Ly9NLiH0tI .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ju3rx5Ly9NLiH0tI .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ju3rx5Ly9NLiH0tI .marker.cross{stroke:#333333;}#mermaid-svg-ju3rx5Ly9NLiH0tI svg{font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ju3rx5Ly9NLiH0tI .label{font-family:\"trebuchet ms\",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ju3rx5Ly9NLiH0tI .cluster-label text{fill:#333;}#mermaid-svg-ju3rx5Ly9NLiH0tI .cluster-label span{color:#333;}#mermaid-svg-ju3rx5Ly9NLiH0tI .label text,#mermaid-svg-ju3rx5Ly9NLiH0tI span{fill:#333;color:#333;}#mermaid-svg-ju3rx5Ly9NLiH0tI .node rect,#mermaid-svg-ju3rx5Ly9NLiH0tI .node circle,#mermaid-svg-ju3rx5Ly9NLiH0tI .node ellipse,#mermaid-svg-ju3rx5Ly9NLiH0tI .node polygon,#mermaid-svg-ju3rx5Ly9NLiH0tI .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ju3rx5Ly9NLiH0tI .node .label{text-align:center;}#mermaid-svg-ju3rx5Ly9NLiH0tI .node.clickable{cursor:pointer;}#mermaid-svg-ju3rx5Ly9NLiH0tI .arrowheadPath{fill:#333333;}#mermaid-svg-ju3rx5Ly9NLiH0tI .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ju3rx5Ly9NLiH0tI .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ju3rx5Ly9NLiH0tI .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-ju3rx5Ly9NLiH0tI .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-ju3rx5Ly9NLiH0tI .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ju3rx5Ly9NLiH0tI .cluster text{fill:#333;}#mermaid-svg-ju3rx5Ly9NLiH0tI .cluster span{color:#333;}#mermaid-svg-ju3rx5Ly9NLiH0tI div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ju3rx5Ly9NLiH0tI :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;} Proxy 代理层 响应式系统 reactive/ref API 双向绑定 v-model
三、vue2 响应式的原理和缺陷
(了解 Object.defineProperty 可跳过本章节。)
在 vue2 中使用 Object.defineProperty
来实现响应式,可以拦截数据读取和赋值操作。
注意:下面代码实现一个最简单的数据劫持,仅仅是数据劫持。
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
演示
// 目标对象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) }})// 操作代理对象的namep_obj.name = \'ls\'const age = p_obj.age// 查看目标对象的 name 属性console.log(\'obj.name: \', obj.name)// !!!新增属性!!!obj.isMale = trueif (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
。
let activeEffect = nullfunction effect(fn) { activeEffect = fn fn() // 立即执行一次,触发依赖收集 activeEffect = null // 执行完成后重置}
当我们调用 effect(fn)
时,Vue 就记录下了正在执行的副作用函数,并在之后数据变动时重新执行它。
4.2 实现 reactive
使用 Proxy 来包裹对象,拦截它的 get
和 set
操作。
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
不一样的是,
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
的结构:
/** * 全局依赖存储 * 结构: WeakMap<target, Map<key, Set>> */// 第一级:目标对象 → 依赖映射// 第二级:属性键 → 依赖集合// 第三级:Set 存储 effectconst 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 响应式工作流
#mermaid-svg-b3NzXVuurt6FmKiD {font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-b3NzXVuurt6FmKiD .error-icon{fill:#552222;}#mermaid-svg-b3NzXVuurt6FmKiD .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-b3NzXVuurt6FmKiD .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-b3NzXVuurt6FmKiD .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-b3NzXVuurt6FmKiD .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-b3NzXVuurt6FmKiD .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-b3NzXVuurt6FmKiD .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-b3NzXVuurt6FmKiD .marker{fill:#333333;stroke:#333333;}#mermaid-svg-b3NzXVuurt6FmKiD .marker.cross{stroke:#333333;}#mermaid-svg-b3NzXVuurt6FmKiD svg{font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-b3NzXVuurt6FmKiD .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-b3NzXVuurt6FmKiD text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-b3NzXVuurt6FmKiD .actor-line{stroke:grey;}#mermaid-svg-b3NzXVuurt6FmKiD .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-b3NzXVuurt6FmKiD .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-b3NzXVuurt6FmKiD #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-b3NzXVuurt6FmKiD .sequenceNumber{fill:white;}#mermaid-svg-b3NzXVuurt6FmKiD #sequencenumber{fill:#333;}#mermaid-svg-b3NzXVuurt6FmKiD #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-b3NzXVuurt6FmKiD .messageText{fill:#333;stroke:#333;}#mermaid-svg-b3NzXVuurt6FmKiD .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-b3NzXVuurt6FmKiD .labelText,#mermaid-svg-b3NzXVuurt6FmKiD .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-b3NzXVuurt6FmKiD .loopText,#mermaid-svg-b3NzXVuurt6FmKiD .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-b3NzXVuurt6FmKiD .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-b3NzXVuurt6FmKiD .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-b3NzXVuurt6FmKiD .noteText,#mermaid-svg-b3NzXVuurt6FmKiD .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-b3NzXVuurt6FmKiD .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-b3NzXVuurt6FmKiD .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-b3NzXVuurt6FmKiD .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-b3NzXVuurt6FmKiD .actorPopupMenu{position:absolute;}#mermaid-svg-b3NzXVuurt6FmKiD .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-b3NzXVuurt6FmKiD .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-b3NzXVuurt6FmKiD .actor-man circle,#mermaid-svg-b3NzXVuurt6FmKiD line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-b3NzXVuurt6FmKiD :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;} 组件 Proxy拦截器 依赖系统 读取数据 (get) 触发get拦截 track(target, key) 存储activeEffect 到WeakMap→Map→Set 修改数据 (set) 触发set拦截 trigger(target, key) 通知关联effect执行 组件 Proxy拦截器 依赖系统
完整代码
/** * 全局依赖存储 * 结构: WeakMap<target, Map<key, Set>> */// 第一级:目标对象 → 依赖映射// 第二级:属性键 → 依赖集合// 第三级:Set 存储 effectconst 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: reactive 基本功能console.log(\'===== reactive测试 =====\')const person = reactive({ name: \'张三\', age: 25})effect(() => { console.log(`个人信息: ${person.name}, ${person.age}岁`)})person.name = \'李四\' // 触发 effectperson.age = 30 // 触发 effect// 测试2: ref 基本功能console.log(\'\\n===== ref测试 =====\')const count = ref(0)effect(() => { console.log(`当前计数: ${count.value}`)})count.value++ // 触发 effectcount.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 // 触发 effectstate.score.value = 90 // 触发 effect
测试输出:
===== reactive测试 =====个人信息: 张三, 25岁个人信息: 李四, 25岁个人信息: 李四, 30岁===== ref测试 =====当前计数: 0当前计数: 1当前计数: 2===== 结合测试 =====学生信息: ID=1, 分数=80学生信息: ID=2, 分数=80学生信息: ID=2, 分数=90
参考
-
Proxy - JavaScript | MDN
-
面试官: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