- 一、为什么 Vue3 从 defineProperty 换成 Proxy
- 二、Proxy 与 Reflect
- 三、核心三要素:依赖收集、触发更新、副作用
- 四、用 WeakMap 组织依赖关系
- 五、reactive:Proxy 的核心实现
- 六、ref:为什么还需要它
- 七、computed 的实现思路
- 八、总结与面试答题话术
“Vue3 的响应式是怎么实现的?”是面试必问题。本文不满足于”用了 Proxy”这个答案,而是带你从零手写一个迷你响应式系统,真正理解
reactive、ref、computed的底层原理。
一、为什么 Vue3 从 defineProperty 换成 Proxy
Vue2 用 Object.defineProperty 劫持对象的已有属性,存在几个硬伤:
- 无法检测新增/删除属性:需要
Vue.set/Vue.delete补救; - 数组索引/长度变化无法响应:需要重写数组方法(
push/pop等 7 个); - 必须递归遍历:对象嵌套越深,初始化越慢。
Vue3 改用 Proxy 代理整个对象,能拦截所有操作(读取、赋值、删除、in、for...in、函数调用),且是惰性代理——访问到深层时才递归代理,性能更好。
二、Proxy 与 Reflect
Proxy 可以拦截对象的底层操作,Reflect 提供了与之一一对应的静态方法:
const target = { name: 'shanky' }
const proxy = new Proxy(target, {
get(obj, key, receiver) {
console.log('读取', key)
return Reflect.get(obj, key, receiver) // 等价于 obj[key]
},
set(obj, key, value, receiver) {
console.log('设置', key, value)
return Reflect.set(obj, key, value, receiver) // 返回是否成功
},
})
Reflect.set/get的第三个参数receiver是 this 的指向,直接使用obj[key]在遇到 getter/setter 时可能 this 指向错误,所以规范实现都用Reflect。
三、核心三要素:依赖收集、触发更新、副作用
响应式系统围绕三个概念:
- 副作用函数(effect):会读取响应式数据、且希望数据变化时重新执行的函数(渲染函数、computed 等)。
- 依赖收集(track):副作用读取数据时,把”数据 → 副作用”的映射记录下来。
- 触发更新(trigger):数据变化时,找到所有依赖它的副作用并重新执行。
先建一个全局”当前正在执行的副作用”桶:
// 用一个栈保存正在执行的 effect,便于嵌套
let activeEffect
const effectStack = []
function effect(fn) {
const eff = () => {
// 入栈,清空依赖,重新收集
effectStack.push(eff)
activeEffect = eff
cleanup(eff)
fn()
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
eff.deps = new Set() // 记录本 effect 依赖了哪些"桶"
eff()
return eff
}
四、用 WeakMap 组织依赖关系
数据结构和依赖关系如下:
WeakMap target -> Map key -> Set<effect>
- WeakMap:以原始对象为 key,WeakMap 的好处是对象被回收时,对应条目自动清除,避免内存泄漏。
- Map:key 是对象的属性名。
- Set:存放依赖该属性的所有 effect。
const targetMap = new WeakMap()
function track(target, key) {
if (!activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) targetMap.set(target, (depsMap = new Map()))
let dep = depsMap.get(key)
if (!dep) depsMap.set(key, (dep = new Set()))
dep.add(activeEffect)
activeEffect.deps.add(dep)
}
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (dep) {
// 拷贝一份再遍历,避免执行过程中增删影响
[...dep].forEach((eff) => eff())
}
}
五、reactive:Proxy 的核心实现
function reactive(target) {
return new Proxy(target, {
get(obj, key, receiver) {
track(obj, key) // 读取时收集依赖
const res = Reflect.get(obj, key, receiver)
// 惰性代理:如果值是对象,递归代理(注意缓存避免重复代理)
if (isObject(res)) return reactive(res)
return res
},
set(obj, key, value, receiver) {
const hadKey = Object.prototype.hasOwnProperty.call(obj, key)
const oldValue = obj[key]
const ok = Reflect.set(obj, key, value, receiver)
if (!ok) return false
if (!hadKey) {
trigger(obj, key, 'ADD') // 新增属性也能响应(Proxy 的优势)
} else if (oldValue !== value) {
trigger(obj, key, 'SET') // 值真正变化才触发
}
return ok
},
deleteProperty(obj, key) {
const hadKey = Object.prototype.hasOwnProperty.call(obj, key)
const ok = Reflect.deleteProperty(obj, key)
if (ok && hadKey) trigger(obj, key, 'DELETE')
return ok
},
})
}
六、ref:为什么还需要它
reactive 只能代理对象,而基础类型(number/string)没有属性可代理。于是 ref 用一个 { value } 包装对象:
function ref(val) {
const wrapper = { value: val }
// 复用 reactive 的代理逻辑(对包装对象做响应式)
return reactive(wrapper)
}
在模板中 ref 会自动解包(8 而不是 ``),但在 JS 中必须 .value。
顺带一提:Vue3 里
toRef/toRefs用于把reactive的某个属性变成ref,在reactive与setup解构、v-model绑定中非常常用。
七、computed 的实现思路
computed = 惰性 effect + 缓存:只有在依赖变化且被读取时才重新计算。
function computed(getter) {
let value
let dirty = true // 脏标记:是否缓存失效
const effectFn = effect(() => {
value = getter() // 依赖变化时执行
dirty = true // 标记需要重新计算
})
return {
get value() {
if (dirty) {
value = getter()
dirty = false
}
return value
},
}
}
八、总结与面试答题话术
面试被问”Vue3 响应式原理”,一个清晰的回答框架:
- Vue3 用 Proxy 替代 defineProperty:能监听新增/删除/数组索引,惰性递归,解决 Vue2 三大痛点;
- 核心是依赖收集 + 触发更新:
track收集”属性 → effect”的映射,trigger在变化时执行相关 effect; - 数据结构是 WeakMap→Map→Set 三层映射;
- reactive 针对对象,ref 针对基础类型,computed 是带缓存的惰性 effect;
- 能现场写出迷你实现,就是真正的理解而非背书。
掌握这套原理,你不仅能应对面试,也能更自信地阅读 Vue 源码、排查响应式相关的诡异 bug。
