Vue3 响应式原理深析:Proxy 源码拆解、ref/reactive 区别与依赖收集完整流程

Hi,我是前端人类学

Vue3 的响应式系统是一次彻底的重构------从 Object.defineProperty 换装为 Proxy,不仅解决了 Vue2 中新增属性无法响应、数组索引变更无法感知等痛点,更在性能和组织上做了全新设计。

本文将从源码层面拆解其核心机制,理解 refreactive 的本质差异,并走通从依赖收集到派发更新的完整流程。

文章目录

    • [一、为什么是 Proxy?------ Vue2 的痛点与 Vue3 的破局](#一、为什么是 Proxy?—— Vue2 的痛点与 Vue3 的破局)
    • [二、reactive 源码拆解:Proxy 如何代理对象](#二、reactive 源码拆解:Proxy 如何代理对象)
      • [2.1 类型校验与缓存](#2.1 类型校验与缓存)
      • [2.2 可观察性校验](#2.2 可观察性校验)
      • [2.3 创建 Proxy](#2.3 创建 Proxy)
      • [2.4 baseHandlers 的核心:get 与 set](#2.4 baseHandlers 的核心:get 与 set)
    • [三、ref 的本质:为原始值套上响应式外壳](#三、ref 的本质:为原始值套上响应式外壳)
      • [3.1 RefImpl 类](#3.1 RefImpl 类)
      • [3.2 模板自动解包](#3.2 模板自动解包)
      • [3.3 ref vs reactive:如何选择](#3.3 ref vs reactive:如何选择)
    • [四、依赖收集完整流程:track 与 trigger 的协作](#四、依赖收集完整流程:track 与 trigger 的协作)
      • [4.1 activeEffect 与 effect 栈](#4.1 activeEffect 与 effect 栈)
      • [4.2 track:依赖收集](#4.2 track:依赖收集)
      • [4.3 trigger:派发更新](#4.3 trigger:派发更新)
      • [4.4 Vue3.5 的响应式优化:双向链表](#4.4 Vue3.5 的响应式优化:双向链表)

一、为什么是 Proxy?------ Vue2 的痛点与 Vue3 的破局

在 Vue2 中,响应式数据通过 Object.defineProperty 对对象的已有属性逐个进行 getter/setter 劫持。这带来了两个核心限制:

  • 无法检测属性的新增和删除data 初始化时不存在的属性,后续赋值无法触发响应式更新,必须使用 Vue.setthis.$set 来补救。
  • 数组变更的局限性 :直接通过索引修改数组项 arr[0] = x 无法被检测,Vue2 只能通过重写 pushpop 等 7 个数组方法来实现响应式。

Proxy 则完全不同------它代理的是整个对象 ,而非某个具体属性。任何对对象的操作(读取、写入、删除、in 操作等)都会先经过 Proxy 的拦截器。

javascript 复制代码
const data = new Proxy(raw, {
  get(target, key) { /* 拦截任意属性的读取 */ },
  set(target, key, value) { /* 拦截任意属性的写入 */ }
})

这意味着无论属性是否预先存在,只要对代理对象进行操作,拦截器都会生效。新增属性 state.newProp = 'hi' 能自动触发更新,数组索引修改 arr[0] = 10 同样能被捕获。这是 Vue3 响应式系统最底层的进化。

二、reactive 源码拆解:Proxy 如何代理对象

Vue3 中 reactive API 的核心逻辑在 createReactiveObject 函数中。我们拆解其关键步骤:

2.1 类型校验与缓存

typescript 复制代码
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {
  // 非对象直接返回
  if (!isObject(target)) return target;
  
  // 已经是代理对象,直接返回
  if (target[ReactiveFlags.RAW] && !(isReadonly && target[ReactiveFlags.IS_REACTIVE])) {
    return target;
  }
  
  // 从缓存中取,避免重复代理
  const existingProxy = proxyMap.get(target);
  if (existingProxy) return existingProxy;
  
  // ...
}

proxyMap 是一个 WeakMap,以原始对象为 key,代理对象为 value 进行缓存。这样同一个对象多次调用 reactive 会返回同一个代理实例。

2.2 可观察性校验

getTargetType 会检查目标对象的类型是否在白名单中:ObjectArrayMapSetWeakMapWeakSet 可以被代理;否则返回 TargetType.INVALID 直接跳过。

2.3 创建 Proxy

对于普通对象和数组,使用 baseHandlers(即 mutableHandlers);对于 Map/Set 等集合类型,使用 collectionHandlers

typescript 复制代码
const proxy = new Proxy(
  target,
  targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
);
proxyMap.set(target, proxy);
return proxy;

2.4 baseHandlers 的核心:get 与 set

mutableHandlers 是代理行为的核心,包含了 getsetdeletePropertyhasownKeys 等拦截器。

get 拦截器 负责依赖收集(track)和深度代理:

typescript 复制代码
function get(target, key, receiver) {
  // 内置标志位处理
  if (key === ReactiveFlags.IS_REACTIVE) return true;
  
  const res = Reflect.get(target, key, receiver);
  // 依赖收集
  track(target, key);
  
  // 深度响应式:取值时如果结果是对象,递归调用 reactive
  return isObject(res) ? reactive(res) : res;
}

注意这里的惰性代理 :嵌套对象只在被访问到时才被代理,而非初始化时递归遍历所有层级,这带来了性能优势。

set 拦截器 负责触发更新(trigger):

typescript 复制代码
function 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;
}

deleteProperty 同样会调用 trigger,确保删除属性也能触发响应。

三、ref 的本质:为原始值套上响应式外壳

reactive 只能代理对象,对于 numberstringboolean 等原始值无能为力。ref 正是为此而生------它通过一个包装对象让原始值也具备响应性。

3.1 RefImpl 类

ref 的底层是一个 RefImpl 类,核心结构非常简洁:

typescript 复制代码
class RefImpl {
  public __v_isRef = true;
  private _value: any;
  public dep: Set<ReactiveEffect> = new Set();

  constructor(value) {
    // 如果传入的是对象,内部用 reactive 转为响应式
    this._value = isObject(value) ? reactive(value) : value;
  }

  get value() {
    trackRefValue(this); // 依赖收集
    return this._value;
  }

  set value(newVal) {
    if (newVal !== this._value) {
      this._value = isObject(newVal) ? reactive(newVal) : newVal;
      triggerRefValue(this); // 触发更新
    }
  }
}

ref 返回的就是 RefImpl 的实例,通过 get value()set value() 拦截对 .value 的读写,从而实现响应式。ref 的底层并不直接等于 reactive ------只有当传入的值是对象时,才会将对象交给 reactive 处理;对于原始值,ref 自己维护依赖收集和派发更新。

3.2 模板自动解包

在模板中直接使用 ref 时不需要写 .value,这是因为 Vue 在编译阶段对模板中的 ref 做了自动解包处理。另外,当 ref 作为 reactive 对象的属性被访问时,也会自动解包:

javascript 复制代码
const count = ref(0);
const state = reactive({ count });
console.log(state.count); // 0,自动解包,不需要 .value

3.3 ref vs reactive:如何选择

特性 ref reactive
适用类型 任意类型(原始值 + 对象) 仅对象(含数组、集合)
访问方式 .value 直接访问属性
底层实现 RefImpl 类 + .value 拦截 Proxy 代理整个对象
解构响应性 解构失去响应性 解构失去响应性
模板中使用 自动解包,无需 .value 直接使用

使用建议 :对于基础类型和不确定类型的值,优先用 ref;对于结构清晰的对象状态,用 reactive 更自然。如果实在不确定,ref 是更通用的选择

四、依赖收集完整流程:track 与 trigger 的协作

依赖收集是响应式系统的心脏。Vue3 的依赖管理以 targetMap 为核心数据结构,它是一个 WeakMap,结构为:

text 复制代码
targetMap: WeakMap<target, Map<key, Set<ReactiveEffect>>>

即:目标对象 → (属性名 → 副作用函数集合)

4.1 activeEffect 与 effect 栈

在依赖收集期间,需要一个全局变量记录当前正在执行的副作用函数 ------这就是 activeEffecteffect 函数(watchEffectrender 等底层都依赖它)在执行前会把自身设置为 activeEffect,执行完毕后重置。

typescript 复制代码
class ReactiveEffect {
  run() {
    try {
      effectStack.push((activeEffect = this));
      return this.fn(); // 执行用户函数,触发 get 拦截
    } finally {
      effectStack.pop();
      activeEffect = effectStack[effectStack.length - 1];
    }
  }
}

effectStack 用于处理嵌套 effect 场景(如组件嵌套),确保依赖始终收集到正确的副作用函数上。

4.2 track:依赖收集

当访问响应式数据的属性时,触发 get 拦截器,内部调用 track 函数:

typescript 复制代码
function track(target, key) {
  if (!activeEffect) return; // 没有活跃的 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);
}

track 做的事情很纯粹:建立 target → key → effect 的映射关系activeEffect 作为当前正在运行的副作用函数,被收入对应 key 的依赖集合中。

4.3 trigger:派发更新

当修改响应式数据的属性时,触发 set 拦截器,内部调用 trigger 函数:

typescript 复制代码
function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  
  const dep = depsMap.get(key);
  if (dep) {
    dep.forEach(effect => effect.run()); // 遍历执行所有副作用函数
  }
}

triggertargetMap 中取出该属性对应的所有依赖(副作用函数),逐个执行,完成更新。

4.4 Vue3.5 的响应式优化:双向链表

Vue3.5 对响应式系统做了重大重构,引入了**双向链表(Link)**连接 Dep(依赖)和 Sub(订阅者),替代了之前 depeffect 的直接关联。这一重构带来了 56% 的性能提升

在新的模型中:

  • Dep (响应式变量)和 Sub(副作用函数)之间不再直接引用。
  • Link 作为桥梁,同时持有 depsub 引用,并通过 nextDep/prevDepnextSub/prevSub 形成 X 轴和 Y 轴的双向链表结构。

依赖触发时,从 Dep 指向的链表尾部开始,沿 Y 轴依次访问所有 Sub,执行对应的副作用函数。这种结构使得依赖的增删和遍历更加高效。


Vue3 的响应式系统可以概括为三层:

  1. 代理层Proxy 拦截对象操作,reactive 负责对象响应式,RefImpl 为原始值提供 .value 包装。
  2. 依赖管理层targetMaptarget → key → Set(effect) 的层级存储依赖,track 收集,trigger 派发。
  3. 调度执行层activeEffecteffectStack 管理当前活跃的副作用函数,Vue3.5 进一步用双向链表优化了依赖的组织与遍历。

理解这一套机制,既能帮你在日常开发中更自信地选择 ref 还是 reactive,也能让你在遇到响应式失效的"怪问题"时,快速定位根因------而不再需要靠"加个 setTimeout 试试"来碰运气。

相关推荐
行者-全栈开发3 个月前
开源项目深度解析:Vue.js 响应式原理与源码拆解
proxy·响应式原理·源码解读·技术实操·vue.js 源码·依赖收集·开源项目解析