深入浅出 Vue3 响应式原理:从 Proxy 到手写核心代码

0 阅读2分钟

🔥 深入浅出 Vue3 响应式原理:从 Proxy 到手写核心代码

前言

只要你是 Vue 开发者,面试时一定逃不开这个问题:“请说一下 Vue 的响应式原理”。 很多同学能背出“Vue2 用 Object.defineProperty,Vue3 用 Proxy”,但如果面试官继续深挖:“为什么用 Proxy?依赖是怎么收集的?Reflect 有什么用?”可能就会一脸懵。

今天,我们就用最通俗易懂的语言,由简入深地扒开 Vue3 响应式系统的外衣,最后带你手写一个 Mini 版的 Vue3 响应式核心!🚀


1. 为什么要抛弃 Object.defineProperty?

在讲 Vue3 之前,我们先鞭尸一下 Vue2。Vue2 使用 Object.defineProperty 来劫持对象的 gettersetter。但它有几个致命的缺点:

  1. 无法监听对象属性的新增和删除(所以才有了 $set$delete)。
  2. 无法原生监听数组的索引和长度变化(Vue2 内部 hack 了数组的方法)。
  3. 必须深层遍历:如果对象层级很深,Vue2 在初始化时就会递归遍历所有属性,非常消耗性能。

为了解决这些痛点,Vue3 拥抱了 ES6 的新特性:Proxy


2. 核心基石:Proxy 与 Reflect

什么是 Proxy?

Proxy 顾名思义就是“代理”。你可以把它理解为对象外层的一层**“安检门”**。无论你是想读取对象的属性,还是修改对象的属性,都必须经过这扇门。

const target = { name: '尤雨溪', age: 18 };

const proxy = new Proxy(target, {
  get(target, key) {
    console.log(`👀 拦截到了读取:${key}`);
    return target[key];
  },
  set(target, key, value) {
    console.log(`✍️ 拦截到了设置:${key} = ${value}`);
    target[key] = value;
    return true;
  }
});

proxy.name; // 控制台输出:👀 拦截到了读取:name
proxy.age = 20; // 控制台输出:✍️ 拦截到了设置:age = 20

Proxy 的优势:它代理的是整个对象,而不是对象的某个属性。所以无论是新增属性还是删除属性,甚至数组的变化,它都能拦截到!而且它是惰性的,只有你访问到深层属性时,才会去代理深层属性。

为什么还需要 Reflect?

在 Vue3 的源码中,Proxy 永远是和 Reflect 结对出现的。为什么不直接 return target[key] 呢?

核心原因是为了保证 this 的指向正确

假设我们有这样一个对象:

const obj = {
  firstName: '尤',
  lastName: '雨溪',
  get fullName() {
    return this.firstName + this.lastName;
  }
};

如果我们在 Proxyget 中直接 return target[key],当访问 proxy.fullName 时,fullName 内部的 this 会指向原始对象 obj,而不是代理对象 proxy。这会导致 firstNamelastName 的读取无法被拦截

Reflect.get(target, key, receiver) 中的 receiver 就是代理对象本身,它能把 this 纠正为代理对象。


3. 响应式系统的三大件:effect、track、trigger

有了 Proxy 拦截数据还不够,我们还需要知道:数据变化时,到底该通知谁去更新?

Vue3 响应式系统有三个核心概念:

  1. effect (副作用函数):你可以把它理解为“谁在使用数据”。比如组件的渲染函数、watch 回调等。
  2. track (依赖收集):在 Proxy 的 get 中触发。把当前的 effect 记录下来。
  3. trigger (派发更新):在 Proxy 的 set 中触发。数据变了,把之前记录的 effect 拿出来执行一遍。

依赖是怎么存储的?(重点!)

Vue3 设计了一个非常巧妙的数据结构来存储依赖,它是一个三层嵌套的结构:WeakMap -> Map -> Set

  • WeakMap:它的 key 是目标对象(target),value 是一个 Map。(使用 WeakMap 是为了防止内存泄漏,对象销毁时依赖也会自动回收)。
  • Map:它的 key 是对象的属性名(key),value 是一个 Set
  • Set:里面存的就是一个个的 effect 函数(因为同一个属性可能被多个地方使用,Set 可以去重)。

结构图如下:

WeakMap {
  { name: 'Vue3', age: 3 } : Map {
    'name' : Set [ effect1, effect2 ],
    'age'  : Set [ effect3 ]
  }
}

4. 手写一个 Mini 版响应式系统

纸上得来终觉浅,绝知此事要躬行。我们把上面的理论转化为代码!

// 1. 存储依赖的全局结构
const targetMap = new WeakMap();

// 2. 记录当前正在执行的 effect
let activeEffect = null;

// 3. effect 函数:包装用户的回调
function effect(fn) {
  const effectFn = () => {
    activeEffect = effectFn; // 执行前,把自己暴露到全局
    fn(); // 执行用户函数,这会触发 Proxy 的 get
    activeEffect = null; // 执行完,清理掉
  };
  effectFn(); // 立即执行一次,完成初始的依赖收集
}

// 4. track:依赖收集
function track(target, key) {
  if (!activeEffect) return; // 如果没有 activeEffect,说明不是在 effect 中读取的,不管它

  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); // 把当前的 effect 存进去!
}

// 5. trigger:派发更新
function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;

  const dep = depsMap.get(key);
  if (dep) {
    dep.forEach(effectFn => effectFn()); // 数据变了,把存起来的 effect 全都执行一遍!
  }
}

// 6. reactive:创建响应式对象
function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      // 收集依赖
      track(target, key);
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
      const result = Reflect.set(target, key, value, receiver);
      // 派发更新
      trigger(target, key);
      return result;
    }
  });
}

测试一下我们的代码!

const state = reactive({ count: 0, name: 'Vue' });

// 模拟组件渲染
effect(() => {
  console.log(`🔄 视图更新啦!当前 count 为:${state.count}`);
});
// 初始打印:🔄 视图更新啦!当前 count 为:0

console.log('--- 修改数据 ---');
state.count++; 
// 打印:🔄 视图更新啦!当前 count 为:1
state.count++; 
// 打印:🔄 视图更新啦!当前 count 为:2

// 修改未在 effect 中使用的属性,不会触发更新
state.name = 'Vue3'; 

总结

Vue3 的响应式原理其实就是一场**“发布-订阅”**的精妙演出:

  1. reactive 利用 Proxy 设立了安检门,结合 Reflect 保证了 this 的绝对正确。
  2. effect 是舞台上的演员,它在登台(执行)前会大喊一声“我现在是 activeEffect!”。
  3. track 是安检门的记录员(get 拦截),它看到 activeEffect 访问了某个属性,就把他记在小本本(WeakMap -> Map -> Set)上。
  4. trigger 是安检门的广播员(set 拦截),一旦有人修改了属性,他就翻开小本本,把记录在案的演员(effect)全都叫出来重新表演一次。

希望这篇文章能帮你彻底搞懂 Vue3 的响应式原理!如果觉得有帮助,别忘了点赞收藏哦~ 👍✨