Skip to content

6.2 源码解析:Vue 3 响应式核心

你是否曾对 Vue 3 那“魔法”般的响应式系统感到好奇?为什么当你修改一个在 setup 中定义的 refreactive 对象时,相关的视图就会自动、精确地更新?

这背后没有魔法,而是经典设计模式与现代 JavaScript 特性精妙结合的产物。本章,我们将深入其核心,揭示这个魔法背后的两大支柱:代理模式 (Proxy Pattern)观察者模式 (Observer Pattern)

一、核心武器:ES6 Proxy

Vue 3 的响应式系统之所以比 Vue 2 (基于 Object.defineProperty) 更强大、性能更好,根本原因在于它使用了 ES6 的 Proxy 对象。

Proxy代理模式在 JavaScript 语言层面的原生实现。它允许我们创建一个对象的“代理”,从而可以拦截并自定义该对象上的基本操作(如读取、赋值等)。

让我们看一个最简单的 Proxy 示例:

javascript
const user = { name: "张三", age: 20 };

const userProxy = new Proxy(user, {
  // `get` 陷阱:当读取代理对象的属性时触发
  get(target, key) {
    console.log(`有人读取了 ${key} 属性`);
    return target[key];
  },
  // `set` 陷阱:当设置代理对象的属性时触发
  set(target, key, value) {
    console.log(`有人设置了 ${key} 属性,新值为: ${value}`);
    target[key] = value;
    return true;
  },
});

userProxy.name; // 控制台输出: 有人读取了 name 属性
userProxy.age = 21; // 控制台输出: 有人设置了 age 属性,新值为: 21

Vue 3 响应式的核心,就是构建在这两个强大的“陷阱”之上的。

二、代理模式:拦截所有操作

当你调用 reactive(obj) 时,Vue 返回的并不是原始的 obj 对象,而是一个包裹了 objProxy 实例。

get 陷阱:我是谁?我在依赖谁? (依赖收集 - Track)

当一个组件进行渲染时,它会执行其 render 函数,这个函数会读取响应式数据(例如 state.count)。这个“读取”操作,就会被代理的 get 陷阱捕捉到。

get 陷阱在此时的核心任务是“依赖收集” (Track)。它就像一个尽职的秘书,会记录下:

“OK,我记下了,是组件 A 的渲染函数,依赖了state 对象的 count 属性。”

这个“依赖关系”会被存储在一个全局的数据结构中(通常是一个 WeakMap)。

set 陷阱:我变了!快去更新! (派发更新 - Trigger)

当你通过某个事件(如点击按钮)修改了响应式数据时(例如 state.count++),这个“写入”操作,就会被代理的 set 陷阱捕捉到。

set 陷阱在此时的核心任务是“派发更新” (Trigger)。它就像一个广播员,会大声宣布:

“注意!state 对象的 count 属性刚刚发生了变化!所有依赖过我的人,请注意!”

然后,它会去那个全局的数据结构里,找到所有依赖了 state.count 的函数(比如我们之前记录的“组件 A 的渲染函数”),并通知它们重新执行。

三、观察者模式:建立数据与视图的连接

“依赖收集”和“派发更新”的整个机制,正是观察者模式的完美实现。

回顾观察者模式定义了对象间一种一对多的依赖关系。当一个对象(被观察者)的状态发生改变时,所有依赖于它的对象(观察者们)都将得到通知并自动更新。

在 Vue 3 的世界里:

  • 被观察者 / 主题 (Subject):就是每一个响应式数据属性(如 state.count)。
  • 观察者 / 订阅者 (Observer):就是那些依赖了数据的副作用函数 (Effect)。最常见的 Effect,就是每个组件的 render 函数。

完整的响应式流程如下:

  1. 挂载阶段:组件 A 首次渲染。Vue 会用一个 effect 函数包裹住组件 Arender 函数。
  2. 执行渲染render 函数被执行,读取了 state.count
  3. 依赖收集 (Track):Proxy 的 get 陷阱被触发。它将当前的 effect (即组件 A 的渲染函数) 作为观察者,添加到 state.count 这个被观察者的“订阅者列表”中。
  4. 数据变更:用户操作导致 state.count++
  5. 派发更新 (Trigger):Proxy 的 set 陷阱被触发。它根据 state.count,从“订阅者列表”中找到了组件 Arender 函数。
  6. 重新渲染:通知组件 Arender 函数(即对应的 effect)重新执行,视图随之更新。

四、动手实现一个 mini-vue 响应式

为了彻底理解,让我们动手写一个极简版的 Vue 3 响应式核心。

javascript
// 全局变量,用于存储当前的 effect
let activeEffect = null;
// 全局 WeakMap,用于存储依赖关系 { target -> { key -> Set<effect> } }
const targetMap = new WeakMap();

// 副作用函数
function effect(fn) {
  activeEffect = fn;
  fn();
  activeEffect = null;
}

// 依赖收集
function track(target, key) {
  if (activeEffect) {
    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);
  }
}

// 派发更新
function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  const dep = depsMap.get(key);
  if (dep) {
    dep.forEach((effect) => effect());
  }
}

// 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 res = Reflect.set(target, key, value, receiver);
      trigger(target, key); // 派发更新
      return res;
    },
  });
}

// --- 测试 ---
const state = reactive({ count: 0 });

effect(() => {
  // 这个函数会在初始化时执行一次,并收集依赖
  console.log(`当前 count 值是: ${state.count}`);
});

// 当我们修改 state.count 时,上面的 effect 函数会自动重新执行!
setInterval(() => {
  state.count++;
}, 1000);

这个 mini-vue 完美地展示了代理模式和观察者模式是如何协同工作的。

五、总结

Vue 3 的响应式系统是一个将设计模式运用到极致的杰作:

  • 它以代理模式为基础,构建了一个能拦截所有数据操作的坚实骨架。
  • 在骨架之上,它通过观察者模式精确地建立了数据(被观察者)与副作用(观察者)之间的依赖关系,实现了高效的、精准的更新。

理解了这一点,Vue 对你来说,就不再是“魔法”,而是一套清晰、优雅的工程设计。