你好,我是大布布将军,一个喜欢把技术原理扒得底裤都不剩的前端显微镜。
今天咱们不聊 API 怎么调,咱们来聊聊 Vue 的灵魂 ------响应式系统。
很多兄弟在面试时都被问过:"Vue 2 和 Vue 3 的响应式有啥区别?" 大部分人只能背出:"Vue 2 用 defineProperty,Vue 3 用 Proxy。" 面试官追问:"那 Proxy 好在哪?具体怎么实现的?" 这时候,空气通常会突然安静...... 😅
别慌!今天咱们就来一场"闭门交流",扒一扒 Vue 3 为什么抛弃了勤勤恳恳的"老兵" Object.defineProperty,转而拥抱了 ES6 的"新贵" Proxy。看完这篇,下次面试你直接给面试官手写一个响应式系统,稳了!
一、Vue 2 的痛:那个勤奋但死板的"门卫"
在 Vue 2 的时代,响应式系统的核心是 Object.defineProperty。你可以把它想象成一个非常勤奋但脑子不太转弯的门卫。
当 Vue 2 初始化一个组件时,它会拿着你的 data 对象,挨个属性遍历,给每个属性都安插一个"门卫"(getter/setter)。
痛点一:无法预知未来(属性增删)
门卫只能看守已经存在的属性。
//
data() {
return {
userInfo: {
name: '老王'
}
}
}
// 后来...
this.userInfo.age = 18; // 门卫:???这谁?我不认识,不管!
因为 age 是后来加的,门卫没登记过,所以你改了 age,视图压根不会理你。 解决办法? 被迫祭出祖传补丁 Vue.set() 或者 this.$set()。这感觉就像是你买了新家具,还得专门去派出所给家具报个户口,麻烦不?
痛点二:数组的"特殊待遇"
如果你有一个包含 1000 个对象的数组,Vue 2 如果要用 defineProperty 给每个索引(0, 1, 2...)都安插门卫,那性能直接原地爆炸。
所以 Vue 2 选择了"偷懒":它重写了数组的 7 个方法(push, pop, splice 等)。 这意味着:
- ✅
arr.push(1)-> 响应式触发。 - ❌
arr[0] = 100-> 门卫不理你。 - ❌
arr.length = 0-> 门卫依然不理你。
✨ 二、Vue 3 的救星:Proxy(拦截器)
Vue 3 引入了 ES6 的 Proxy,彻底改变了玩法。
如果说 defineProperty 是给每个房间门口站个岗,那 Proxy 就是直接给整个房子罩了一层激光防御网。
不管你是想进房间、爬窗户、还是拆墙(增删属性),只要你触碰了这个对象,Proxy 全都知道。
Proxy 强在哪?
- 全方位拦截: 能够拦截对象的 13 种操作(读、写、删除、遍历等)。
- 惰性处理: Vue 2 是一上来就递归遍历所有层级,把所有属性都变成响应式(初始化慢)。Vue 3 是你访问到哪一层,我才代理哪一层 (由
reactive实现),性能大大提升。 - 数组完美支持: 再也不用重写数组方法了,下标修改也能拦截!
✨ 三、手写一个"迷你" Vue 3 响应式系统
光说不练假把式。咱们用几十行代码,还原 Vue 3 响应式的核心逻辑。
核心三件套:
targetMap:存数据的桶。track(追踪) :谁用了这个数据?记录下来(收集依赖)。trigger(触发) :数据变了,通知那些用过的人(触发更新)。
第一步:准备存储结构
我们需要一个地方来存放"谁依赖了哪个对象的哪个属性"。这个结构有点绕,但逻辑很清晰: WeakMap (存对象) -> Map (存属性) -> Set (存副作用函数)
//
const targetMap = new WeakMap();
// 当前正在运行的副作用函数(也就是哪个组件正在读取数据)
let activeEffect = null;
第二步:收集依赖 (Track)
当代码读取属性(触发 get)时,我们把当前的 activeEffect 存到桶里。
function
if (!activeEffect) return; // 如果没有人在依赖,就不管
// 1. 找对象
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
// 2. 找属性
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
// 3. 存入依赖(去重)
dep.add(activeEffect);
}
第三步:触发更新 (Trigger)
当代码修改属性(触发 set)时,我们要去桶里把对应的函数拿出来执行一遍。
function
const depsMap = targetMap.get(target);
if (!depsMap) return; // 这个对象没人关注,散了吧
const dep = depsMap.get(key);
if (dep) {
// 遍历所有依赖这个属性的函数,执行它们
dep.forEach(effect => effect());
}
}
第四步:Proxy 登场!(Reactive)
现在把上面两步串起来。
function
return new Proxy(target, {
// 拦截读取操作
get(target, key, receiver) {
// 🎯 关键点:有人读了,赶紧记录下来!
track(target, key);
// Reflect 是 Proxy 的好基友,保证上下文(this)正确
return Reflect.get(target, key, receiver);
},
// 拦截设置操作
set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
// 🎯 关键点:值变了?赶紧通知大家更新!
if (oldValue !== value) {
trigger(target, key);
}
return result;
}
});
}
✨ 四、跑起来试试?
我们要模拟一个 Vue 的 effect(副作用),你可以把它理解为 Vue 的组件渲染函数。
//
function effect(fn) {
activeEffect = fn; // 标记当前正在运行的函数
fn(); // 立即执行一次,触发 get,从而完成依赖收集
activeEffect = null; // 执行完复位
}
// --- 测试开始 ---
const user = reactive({ name: '老王', age: 30 });
let myText = '';
// 假设这是组件的模板渲染
effect(() => {
console.log('👀 渲染函数执行了!');
myText = `${user.name} 今年 ${user.age} 岁`;
});
console.log(myText);
// 输出: "老王 今年 30 岁" (首次渲染)
console.log('--- 准备修改数据 ---');
// 修改数据,应该会自动触发上面的 effect
user.age = 31;
// 控制台自动输出: "👀 渲染函数执行了!"
console.log(myText);
// 输出: "老王 今年 31 岁" (更新成功!)
看到没?我们没有调用任何更新函数,只是简单的 user.age = 31,myText 就自动更新了!这就是响应式的魔法。
✨ 总结一下
Vue 3 的响应式系统之所以强大,全靠 Proxy 这个"全能管家"。
- 它解决了
defineProperty的先天不足(无法监听新增属性、数组下标)。 - 它配合
WeakMap、Map、Set构建了一套精确的依赖收集系统。 - 它让代码更干净 ,我们不需要再写
Vue.set这种奇怪的代码了。
当然,Vue 3 源码中还有很多复杂的边界处理(比如嵌套对象怎么处理?数组长度修改怎么处理?ref 又是怎么回事?),但核心原理就在这几十行代码里。
下次面试官再问你,你就把这段代码甩给他,告诉他:"这就是 Vue 3 的内功心法!"

🔥 觉得有收获?点个赞/在看,下期咱们聊聊 Vue 3 的 Composition API 到底是怎么吊打 Mixin 的!