告别“祖传”defineProperty!Vue 3 靠 Proxy 练就了什么“神功”?

你好,我是大布布将军,一个喜欢把技术原理扒得底裤都不剩的前端显微镜。

今天咱们不聊 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 强在哪?

  1. 全方位拦截: 能够拦截对象的 13 种操作(读、写、删除、遍历等)。
  2. 惰性处理: Vue 2 是一上来就递归遍历所有层级,把所有属性都变成响应式(初始化慢)。Vue 3 是你访问到哪一层,我才代理哪一层 (由 reactive 实现),性能大大提升。
  3. 数组完美支持: 再也不用重写数组方法了,下标修改也能拦截!

✨ 三、手写一个"迷你" Vue 3 响应式系统

光说不练假把式。咱们用几十行代码,还原 Vue 3 响应式的核心逻辑。

核心三件套:

  1. targetMap:存数据的桶。
  2. track (追踪) :谁用了这个数据?记录下来(收集依赖)。
  3. 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 = 31myText 就自动更新了!这就是响应式的魔法。


✨ 总结一下

Vue 3 的响应式系统之所以强大,全靠 Proxy 这个"全能管家"。

  1. 它解决了 defineProperty 的先天不足(无法监听新增属性、数组下标)。
  2. 它配合 WeakMapMapSet 构建了一套精确的依赖收集系统
  3. 它让代码更干净 ,我们不需要再写 Vue.set 这种奇怪的代码了。

当然,Vue 3 源码中还有很多复杂的边界处理(比如嵌套对象怎么处理?数组长度修改怎么处理?ref 又是怎么回事?),但核心原理就在这几十行代码里。

下次面试官再问你,你就把这段代码甩给他,告诉他:"这就是 Vue 3 的内功心法!"


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

相关推荐
dorisrv1 小时前
TRAE SOLO 正式版:AI全链路开发的新范式 🚀
前端·trae
小明记账簿_微信小程序1 小时前
antd v3 select自定义下拉框内容失去焦点时会关闭下拉框
前端
码途进化论1 小时前
前端Docker多平台构建自动化实践
前端·javascript·后端
dorisrv1 小时前
React轻量级状态管理方案(useReducer + Context API)
前端
qq_316837751 小时前
uniapp 缓存请求文件时 判断是否有文件缓存 并下载和使用
前端·缓存·uni-app
进击的野人1 小时前
Vue中key的作用与Diff算法原理深度解析
前端·vue.js·面试
打工仔张某2 小时前
React Fiber 原理与实践 Demo
前端
程序员小胖2 小时前
每天一道面试题之架构篇|设计千万级高并发点赞/收藏系统架构
面试·架构
chalmers_152 小时前
require 根据工程目录的相对路径-require新文件实现简单的热更新
linux·前端·javascript