VUE响应式基础-ref()、reactive()

vue 496849654@qq.com 3小时前 22次浏览

reactive‌:使用 Proxy 代理对象。
‌ref‌:使用一个包含 _value 属性的普通对象(RefImpl),通过 ES6 Class 的 get/set 访问器手动调用 track 和 trigger,‌不使用‌ Object.defineProperty,也‌不‌对 Ref 本身套 Proxy。
‌依赖管理‌:使用 WeakMap 存储依赖关系,支持嵌套对象和自动解包。
为了更直观地理解 ref 和 reactive 的底层运行逻辑,这里提供一份‌简化版的手写实现代码‌。

🚀 简易响应式系统完整实现

/**
 * ==========================================
 * 1. 依赖收集与触发系统 (Dependency System)
 * ==========================================
 */

// 全局状态:存储当前正在执行的副作用函数
let activeEffect = null;

// 依赖映射表:Target -> Key -> Set<Effects>
// 使用 WeakMap 防止内存泄漏(当对象被垃圾回收时,依赖自动清除)
const targetMap = new WeakMap();

/**
 * 依赖收集:当读取数据时,将当前 effect 记录到依赖表中
 * @param {Object} target - 目标对象
 * @param {String|Symbol} key - 属性键
 */
function track(target, key) {
  if (!activeEffect) return; // 没有正在运行的 effect,无需收集

  let depsMap = targetMap.get(target);
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()));
  }

  let dep0 = depsMap.get(key);
  if (!dep) {
    depsMap.set(key, (dep = new Set()));
  }

  // 避免重复添加同一个 effect
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect);
  }
}

/**
 * 触发更新:当数据修改时,执行所有依赖该数据的 effect
 * @param {Object} target - 目标对象
 * @param {String|Symbol} key - 属性键
 */
function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;

  const effects = depsMap.get(key);
  if (effects) {
    // 创建副本执行,防止 effect 内部修改数据导致无限循环或集合修改错误
    new Set(effects).forEach(effect => effect());
  }
}

/**
 * 注册副作用函数
 * @param {Function} fn - 副作用函数
 */
function effect(fn) {
  const wrappedEffect = () => {
    try {
      activeEffect = wrappedEffect; // 设置当前激活的 effect
      return fn(); // 执行函数,期间会触发 getter 从而调用 track
    } finally {
      activeEffect = null; // 执行完毕,重置
    }
  };
  wrappedEffect(); // 立即执行一次以收集初始依赖
  return wrappedEffect;
}

/**
 * ==========================================
 * 2. Reactive 实现 (基于 Proxy)
 * ==========================================
 */

/**
 * 将对象转换为响应式代理
 * @param {Object} obj 
 * @returns {Proxy}
 */
function reactive(obj) {
  // 仅处理非空对象
  if (typeof obj !== 'object' || obj === null) {
    return obj;
  }

  // 如果已经是响应式对象,直接返回(避免重复代理)
  if (obj.__v_isReactive) return obj;

  const handler = {
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver);
      
      // 【核心】收集依赖
      track(target, key);

      // 【核心】深层响应:如果获取的值是对象,递归转为 reactive
      // 注意:这里只在访问时才转换,属于懒代理
      if (typeof res === 'object' && res !== null) {
        return reactive(res);
      }
      
      return res;
    },
    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(target, key) {
      const hadKey = key in target;
      const result = Reflect.deleteProperty(target, key);
      if (hadKey) {
        trigger(target, key);
      }
      return result;
    }
  };

  const proxy = new Proxy(obj, handler);
  
  // 标记为响应式对象,用于判断
  Object.defineProperty(proxy, '__v_isReactive', {
    value: true,
    enumerable: false,
    writable: false
  });
  
  return proxy;
}

/**
 * ==========================================
 * 3. Ref 实现 (基于 Class Getter/Setter)
 * ==========================================
 */

/**
 * 辅助函数:将值转换为响应式(如果是对象则用 reactive,否则保持原样)
 */
function convert(val) {
  return typeof val === 'object' && val !== null ? reactive(val) : val;
}

/**
 * Ref 类:包装基础类型或对象
 * Vue 3 中 ref 并不使用 defineProperty,也不对 Ref 实例本身做 Proxy
 * 而是通过类的 get/set 访问器手动调用 track/trigger
 */
class RefImpl {
  constructor(rawValue) {
    this._rawValue = rawValue; // 保存原始值
    this._value = convert(rawValue); // 保存转换后的值(对象会被转为 reactive)
    this.__v_isRef = true; // 标记为 Ref
  }

  get value() {
    // 【核心】读取 .value 时,收集对 'value' 这个 key 的依赖
    // 注意:这里的 target 是 RefImpl 实例本身
    track(this, 'value');
    return this._value;
  }

  set value(newVal) {
    if (newVal !== this._rawValue) {
      this._rawValue = newVal;
      this._value = convert(newVal);
      // 【核心】修改 .value 时,触发对 'value' 这个 key 的更新
      trigger(this, 'value');
    }
  }
}

/**
 * 创建 ref
 * @param {*} value 
 * @returns {RefImpl}
 */
function ref(value) {
  return new RefImpl(value);
}

/**
 * 判断是否为 ref
 */
function isRef(r) {
  return !!(r && r.__v_isRef);
}

/**
 * 解包 ref:在模板或 reactive 中自动解包
 */
function unref(refObj) {
  return isRef(refObj) ? refObj.value : refObj;
}

💡 代码运行演示与解析

1. 测试 reactive (对象代理)

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

effect(() => {
  console.log(`[Reactive Effect] Count is: ${state.count}, Name is: ${state.user.name}`);
});
// 输出: [Reactive Effect] Count is: 0, Name is: Alice

state.count++; 
// 输出: [Reactive Effect] Count is: 1, Name is: Alice

state.user.name = 'Bob';
// 输出: [Reactive Effect] Count is: 1, Name is: Bob

2. 测试 ref (基础类型包装)

const countRef = ref(10);

effect(() => {
  console.log(`[Ref Effect] CountRef is: ${countRef.value}`);
});
// 输出: [Ref Effect] CountRef is: 10

countRef.value = 20;
// 输出: [Ref Effect] CountRef is: 20

3. 测试 ref 包裹对象 (自动转换)

const userRef = ref({ name: 'Vue' });

effect(() => {
  // 注意:userRef 是 RefImpl 实例,访问 .value 拿到的是 reactive 代理对象
  // 所以 userRef.value.name 能触发响应式
  console.log(`[Ref Object Effect] User is: ${userRef.value.name}`);
});
// 输出: [Ref Object Effect] User is: Vue

userRef.value.name = 'React';
// 输出: [Ref Object Effect] User is: React

// 整体替换 ref 的对象
userRef.value = { name: 'Svelte' };
// 输出: [Ref Object Effect] User is: Svelte

🔑 核心差异总结(通过代码视角)

对比维度 reactive 代码实现细节 ref 代码实现细节
底层核心机制 直接通过 new Proxy(target, handler) 代理目标对象,完全基于 ES6 Proxy 拦截能力 基于 ES6 Class 定义 RefImpl 类,通过类的 get value() / set value() 访问器实现拦截,不使用 Proxy 包装 Ref 实例本身
依赖收集触发点 在 Proxy 的 get 陷阱中自动执行 track(target, key),key 为对象的具体属性名 在类的 get value() 方法中手动调用 track(this, 'value'),key 固定为字符串 'value'
更新触发逻辑 在 Proxy 的 set 陷阱中自动执行 trigger(target, key),仅属性值真正变化时触发 在类的 set value() 方法中手动调用 trigger(this, 'value'),仅新旧值不等时触发
支持数据类型 仅支持对象、数组等引用类型,传入基础类型直接原值返回 全类型兼容,基础类型直接包装,对象类型内部自动调用 reactive 完成转换
深层响应实现 懒代理模式:在 get 陷阱中递归调用 reactive(res),访问嵌套属性时才完成代理 复用 reactive 能力:通过内部 convert 函数,将传入的对象自动转为 reactive 代理实现深层响应
整体替换行为 无法直接整体替换代理对象,赋值新对象会断开 Proxy 链接,响应式完全失效 支持整体替换:直接给 .value 赋值新对象,新值会自动走响应式转换流程,响应式完整保留
内存标记方式 给 Proxy 实例挂载不可枚举属性 __v_isReactive 标记身份 直接在 RefImpl 类的构造函数中挂载 __v_isRef 属性标记身份
初始化性能表现 仅代理外层对象,嵌套对象访问时才代理,初始化开销低 基础类型无额外代理开销,对象类型初始化时仅做一次 reactive 转换,整体性能更轻量

在 Vue 3 开发中,选择 ref 还是 reactive 主要取决于‌数据类型‌、‌代码维护习惯‌以及‌业务场景的复杂度‌。
虽然两者都能实现响应式,但为了代码的可读性、一致性和避免常见的“响应式丢失”坑点,业界目前更倾向于‌优先使用 ref‌。以下是具体的选型建议和最佳实践:
🏆 核心结论:优先使用 ref
Vue 官方文档及社区主流观点建议:‌在大多数场景下,统一使用 ref 是更合理的选择。‌
‌理由‌:ref 语法统一(无论基础类型还是对象都用 .value),避免了在 ref 和 reactive 之间切换思维模式的认知负担,且彻底规避了 reactive 解构丢失响应式、整体替换失效等痛点。

📊 详细选型指南

1. 必须/推荐使用 ref 的场景

场景 原因与示例
基础数据类型
(String, Number, Boolean)
reactive 不支持基础类型。若强行使用 reactive(1) 会报警告且无响应式。
const count = ref(0)
DOM 或组件实例引用 模板中的 ref="xxx" 属性必须配合脚本中的 ref(null) 使用,用于获取 DOM 元素或子组件实例。
const inputRef = ref(null)
需要频繁解构或传递单个属性 ref 本身就是独立的响应式引用,解构或作为参数传递时不会丢失响应式。
const { name } = toRefs(state) 或直接传递 ref 变量。
希望代码逻辑高度统一 团队规范中若希望所有响应式数据都通过 .value 访问,以减少“这个变量要不要加 .value”的判断成本。
const user = ref({ name: 'Vue' })
需要整体替换对象 当你需要用一个新对象完全替换旧对象时,ref 能完美保持响应式链接,而 reactive 会断裂。
state.value = newObj (有效)
state = newObj (无效,丢失响应式)

2. 可以考虑使用 reactive 的场景

场景 原因与示例
大型复杂嵌套对象/表单 当数据结构非常深,且你希望像操作普通 JS 对象一样直接通过 obj.prop 访问,不想到处写 .value 时。
const form = reactive({ user: { name: '', age: 0 } })
访问:form.user.name
与第三方库交互 某些第三方库期望接收一个纯对象引用,而不是一个包含 .value 的包装器。此时使用 reactive 更自然。
性能极度敏感且对象巨大 虽然差异极小,但 ref 包裹对象时内部仍有一层包装。对于极大对象,直接使用 reactive 代理原对象可能在微观层面少一层间接引用(通常可忽略)。

⚠️ ‌注意‌:如果使用 reactive,请务必配合 toRefs 进行解构,否则解构后的变量将失去响应式。

const state = reactive({ name: 'Vue', age: 3 })
// ❌ 错误:name 失去响应式
const { name } = state 
// ✅ 正确:保留响应式
const { name, age } = toRefs(state)

💡 避坑对照表

操作 ref 表现 reactive 表现 建议
定义基础类型 ✅ 正常响应 ❌ 警告且无响应 必选 ref
定义对象/数组 ✅ 正常响应 (内部转 reactive) ✅ 正常响应 推荐 ref‌ (统一风格)
脚本中取值 需加 .value 直接访问属性 ref 需多敲几个字符,但更明确
模板中取值 自动解包,无需 .value 直接访问属性 两者在模板中体验一致
解构赋值 ✅ 保持响应式 (本身就是 ref) ❌ ‌丢失响应式 reactive 必须用 toRefs
整体替换 obj.value = {} 保持响应 obj = {}丢失响应 推荐 ref
获取 DOM ✅ 唯一标准方式 ❌ 不支持 必选 ref

🚀 企业级落地最佳实践建议

‌默认策略‌:项目中‌默认全部使用 ref‌

优点:心智模型统一,不用判断类型;解构安全;替换安全;DOM 引用方便。
缺点:脚本中需要频繁写 .value(可通过 IDE 插件或 ESLint 规则辅助提示)。
‌例外策略‌:仅在以下情况使用 reactive:

你正在处理一个巨大的、深层嵌套的表单对象,且团队约定好‌绝不解构‌该对象,始终通过 form.field.subField 方式访问。
你需要将响应式对象传递给一个不接受 Ref 包装器的遗留代码或第三方库。
‌工具函数搭配‌:

使用 unref() toValue() (Vue 3.3+) 来统一处理可能是 ref 也可能是普通值的参数。
使用 isRef()isReactive() 进行调试或类型守卫。
‌总结一句话‌:除非你有明确的理由(如避免大量 .value 书写且保证不解构),否则‌无脑选 ref‌ 是最安全、最现代、最易维护的选择。


css315 , 版权所有丨如未注明 , 均为原创丨本网站采用BY-NC-SA协议进行授权
转载请注明原文链接:VUE响应式基础-ref()、reactive()
喜欢 (1)