ref的实现与自动解包:Vue3响应式的另一面

2 阅读7分钟

在之前的文章中,我们讲解到 reactive 解决了对象的响应式问题,但面对原始值(如字符串、数字、布尔值),它却无能为力。ref 的出现填补了这个空白,并通过 .value 这个精巧的设计,让所有类型的数据都能被响应式系统管理。

前言:为什么需要 ref?

当我们在使用 reactive 定义个一个响应式对象是,这是没有问题的

const state = reactive({ count: 0 });
state.count++;

但如果我们想单独管理一个原始值呢?

let count = 0;

此时,ref 就派上了用场:

const countRef = ref(0);
countRef.value++;

更神奇的是,在模板中可以直接使用,不需要 .value

<!-- 自动解包 -->
<div>{{ countRef }}</div> 

原始值的响应式包装

为什么需要 .value?

在 Vue3 中,是通过 Proxy 代理实现的响应式系统;但 Proxy 无法对原始值进行代理,只能通过对象包装。于是 Vue3 设计时,通过创建一个包含 .value 属性的对象,对这个对象进行代理:

let count = 0; // 原始值,无法被代理
let countRef = {value: conut};  // 使用 value 包裹成对象,可以被代理

RefImpl 类的实现

为什么需要 RefImpl 类?

在上述代码中,我们确实可以通过 ref 来创建一个包含 .value 的对象,并对这个对象进行代理,但这也会带来新的问题:

const refVal = ref(1);
const reactiveRef = reactive({value : 1});

上述代码中,refValreactiveRef 在实现和使用上没有任何区别,那我们如何区别一个数据到底是不是 ref 呢?

针对这些新引发的问题, Vue3 开发团队实现了 RefImpl 类来解决这些问题。

RefImpl 类的好处

  • 类型标识:使用 class 创建的实例,其原型链上明确指向 RefImpl ;同时在内部维护 __v_isRef 属性标识,使其更加规范。
  • 性能优化: 所有 RefImpl 实例共享同一个原型(RefImpl.prototype),把方法挂在原型上,相比于在每个工厂函数返回的对象上都复制一遍方法,内存利用率更高。
  • 可维护性RefImpl 除了处理基本类型,还需要处理对象类型的特殊情况。如果给 ref 传入一个对象,Vue3 需要将其转换为 reactive,这部分逻辑放在构造函数中处理会非常清晰。
  • 响应式标记RefImpl 在原型上定义了 __v_isRef 属性,这使得内部工具函数(如 isRefunref)能够准确识别 ref 对象。如果使用普通函数,需要手动为每个返回的对象定义这个属性,增加了代码冗余。

RefImpl 类的内部实现

class RefImpl {
   _rawValue; // 原始值(未处理)
   _value; // 响应式值(可能被 reactive 包装)
   __v_isRef;

  constructor(value) {
    // 保存原始值
    this._rawValue = value;
    // ref 直接存值
    this._value = value;
    this.__v_isRef = true; // 标记为 ref
  }

  // 访问 .value 时收集依赖
  get value() {
    track(this, 'value'); // Ref 的依赖收集目标是自身,key 固定为 'value'
    return this._value;
  }

  // 修改 .value 时触发更新
  set value(newValue) {
    // 新旧值不同才更新(避免无效触发)
    if (newValue !== this._rawValue) {
      this._rawValue = newValue;
      // 浅 ref 直接赋值
      this._value = newValue;
      trigger(this, 'value'); // 触发 Ref 的 'value' 属性更新
    }
  }
}

ref 函数的基础实现

function ref(value) {
  return new RefImpl(value, false);
}

ref 的自动解包

在模板中的自动解包

Vue3 模板编译器会自动对 ref 进行 .value 解包:

  • 在模板中直接使用 {{ ref }}
  • 在指令中直接使用 v-bind="ref"
  • 在事件处理中直接使用 ref
function templateCompiler(template, context) {
    // 简化版模板编译
    const refPattern = /\{\{\s*(\w+)\s*\}\}/g;
    let match;
    
    while ((match = refPattern.exec(template)) !== null) {
        const varName = match[1];
        
        if (context[varName] && context[varName].__v_isRef) {
            console.log(`${varName} 是 ref,编译为 ${varName}.value`);
        } else {
            console.log(`${varName} 是普通变量,保持不变`);
        }
    }
    
    // 模拟编译结果
    const compiled = template.replace(refPattern, (match, varName) => {
        return context[varName] && context[varName].__v_isRef 
            ? '${' + varName + '.value}' 
            : '${' + varName + '}';
    });
    return compiled;
}

模版编译在后面的章节会专门介绍。

在 reactive 中的自动解包

function proxyRefs(objectWithRefs) {
  return new Proxy(objectWithRefs, {
    get(target, key) {
      const value = Reflect.get(target, key)
      // 如果是 ref,自动解包返回 .value;否则返回原值
      return isRef(value) ? value.value : value
    },
    set(target, key, newValue) {
      const value = Reflect.get(target, key)
      // 如果原有值是 ref,修改其 .value;否则直接赋值
      if (isRef(value)) {
        value.value = newValue
        return true
      }
      return Reflect.set(target, key, newValue)
    },
  })
}

ref 工具函数集

isRef 类型判断

function isRef(r) {
  return !!(r && r.__v_isRef === true);
}

shallowRef - 浅层 ref

shallowRef 会创建一个只追踪 .value 变化的 ref,对于 .value 中的对象,不进行深层响应式,其适用场景如下:

  • 大型对象,不需要深层响应式
  • 与第三方库集成
  • 性能优化
class RefImpl {
   _rawValue; // 原始值(未处理)
   _value; // 响应式值(可能被 reactive 包装)
   __v_isRef;

  constructor(value, isShallow) {
    // 保存原始值
    this._rawValue = value;
    // 浅 ref 直接存值;深 ref 对对象类型用 reactive 包装
    this._value = isShallow ? value : toReactive(value);
    this.__v_isRef = true; // 标记为 ref
  }

  // 访问 .value 时收集依赖
  get value() {
    track(this, 'value'); // Ref 的依赖收集目标是自身,key 固定为 'value'
    return this._value;
  }

  // 修改 .value 时触发更新
  set value(newValue) {
    // 新旧值不同才更新(避免无效触发)
    if (newValue !== this._rawValue) {
      this._rawValue = newValue;
      // 浅 ref 直接赋值;深 ref 对对象类型用 reactive 包装
      this._value = this.isShallow ? newValue : toReactive(newValue);
      trigger(this, 'value'); // 触发 Ref 的 'value' 属性更新
    }
  }
}

function shallowRef(value) {
  return new RefImpl(value, true);
}

toRef - 为响应式对象的属性创建 ref

toRef 会为响应式对象的属性创建一个 ref,这个 ref 保持与原属性的响应式连接,其适用场景如下:

  • 将对象的某个属性作为 ref 传递给函数
  • 解构时保持响应式
  • 创建对对象属性的引用
class ObjectRefImpl {
   __v_isRef = true;

  constructor(_object, _key) {}

  // 访问 .value 时指向原对象的属性
  get value() {
    return this._object[this._key]
  }

  // 修改 .value 时同步到原对象的属性
  set value(newValue) {
    this._object[this._key] = newValue
  }
}
// 将对象的单个属性转为 ref(保持引用关系)
function toRef(object, key) {
  return new ObjectRefImpl(object, key)
}

toRefs - 批量转换

toRefs 会将响应式对象的所有属性转换为 ref,保持响应式连接,其适用场景如下:

  • 从 reactive 对象解构属性
  • 在组合式函数中返回多个值
  • 将对象属性作为独立的 ref 使用
// 将对象的所有属性转为 ref(批量 toRef)
function toRefs(object) {
  const result = {};
  // 遍历对象的自有属性
  for (const key in object) {
    if (Object.prototype.hasOwnProperty.call(object, key)) {
      result[key] = toRef(object, key);
    }
  }
  return result;
}

unref - 解包 ref

// 解包 ref:如果是 ref 返回 .value,否则返回自身
function unref(ref) {
  return isRef(ref) ? ref.value : ref
}

完整 ref 的实现

class RefImpl {
   _rawValue; // 原始值(未处理)
   _value; // 响应式值(可能被 reactive 包装)
   __v_isRef;

  constructor(value, isShallow) {
    // 保存原始值
    this._rawValue = value;
    // 浅 ref 直接存值;深 ref 对对象类型用 reactive 包装
    this._value = isShallow ? value : toReactive(value);
    this.__v_isRef = true; // 标记为 ref
  }

  // 访问 .value 时收集依赖
  get value() {
    track(this, 'value'); // Ref 的依赖收集目标是自身,key 固定为 'value'
    return this._value;
  }

  // 修改 .value 时触发更新
  set value(newValue) {
    // 新旧值不同才更新(避免无效触发)
    if (newValue !== this._rawValue) {
      this._rawValue = newValue;
      // 浅 ref 直接赋值;深 ref 对对象类型用 reactive 包装
      this._value = this.isShallow ? newValue : toReactive(newValue);
      trigger(this, 'value'); // 触发 Ref 的 'value' 属性更新
    }
  }
}

// 创建 ref 对象
function ref(value) {
 if (isRef(value)) {
    return value;
  }
  return new RefImpl(value, false);
}

// isRef 类型判断
function isRef(r) {
  return !!(r && r.__v_isRef === true);
}

// 代理 ref 对象:访问时自动解包 .value,修改时自动包裹
function proxyRefs(objectWithRefs) {
  return new Proxy(objectWithRefs, {
    get(target, key) {
      const value = Reflect.get(target, key)
      // 如果是 ref,自动解包返回 .value;否则返回原值
      return isRef(value) ? value.value : value
    },
    set(target, key, newValue) {
      const value = Reflect.get(target, key)
      // 如果原有值是 ref,修改其 .value;否则直接赋值
      if (isRef(value)) {
        value.value = newValue
        return true
      }
      return Reflect.set(target, key, newValue)
    },
  })
}

// 浅 ref:仅 .value 本身响应式,对象类型不递归处理
function shallowRef(value) {
  return new RefImpl(value, true);
}

class ObjectRefImpl {
   __v_isRef = true;

  constructor(_object, _key) {}

  // 访问 .value 时指向原对象的属性
  get value() {
    return this._object[this._key];
  }

  // 修改 .value 时同步到原对象的属性
  set value(newValue) {
    this._object[this._key] = newValue;
  }
}
// 将对象的单个属性转为 ref(保持引用关系)
function toRef(object, key) {
  return new ObjectRefImpl(object, key);
}

// 将对象的所有属性转为 ref(批量 toRef)
function toRefs(object) {
  const result = {};
  // 遍历对象的自有属性
  for (const key in object) {
    if (Object.prototype.hasOwnProperty.call(object, key)) {
      result[key] = toRef(object, key);
    }
  }
  return result;
}

// 解包 ref:如果是 ref 返回 .value,否则返回自身
function unref(ref) {
  return isRef(ref) ? ref.value : ref
}

ref 与 reactive 的选择场景

使用 ref 的场景

  • 基本类型数据(string, number, boolean)
  • 需要解构的属性
  • 在组合式函数中返回的值
  • 需要 .value 显式访问(明确性是优势)
  • 传递给需要 ref 参数的函数

使用 reactive 的场景

  • 对象和数组
  • 深层的响应式数据结构
  • 不需要频繁解构
  • 习惯使用普通对象语法
  • 性能敏感的大对象(少一层代理)

混用建议

  • reactive 中可以包含 ref(会自动解包)
  • ref 中也可以包含对象(但不推荐)
  • 在组件的 setup 中,推荐使用 ref(一致性)
  • Vue 3.2+ 推荐默认使用 ref

结语

ref 系统与 reactive 共同构成了 Vue3 完整的响应式体系,理解它们的实现原理,能够帮助我们在开发中做出更明智的技术选择。对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!