Skip to content

在 Vue 3 的响应式系统中,unreftoRaw 是两个关键的“逃生舱”函数,它们允许开发者穿透响应式代理,访问底层的原始数据。这在某些特定场景下是必需的,因为直接操作代理对象可能会导致意外行为或性能问题。本节将深入解析 unreftoRaw 的实现原理,探讨 toRaw 如何获取原始对象,并阐明其必须使用的典型场景。

一、核心概念:什么是“逃生舱”?

Vue 的响应式系统通过 Proxyref 包装原始数据,以实现依赖追踪和自动更新。然而,这种包装有时会成为障碍:

  • 性能开销:频繁访问深层嵌套的响应式对象可能带来不必要的 track/trigger 开销。
  • 第三方库兼容性:某些库(如日期处理、图表库)无法正确处理 Proxy 对象。
  • 身份比较=== 比较代理对象和原始对象会失败。
  • 序列化问题JSON.stringify 无法正确序列化 Proxy

unreftoRaw 提供了从响应式系统“逃逸”的途径,让开发者能够安全地访问和操作原始数据。


二、unref:统一的值解包工具

unref 是一个通用工具函数,用于“解包”一个值。它的行为取决于输入值的类型。

1. 实现原理

ts
function unref(value) {
  return isRef(value) ? value.value : value;
}
  • 如果 valueref,返回 value.value
  • 否则,直接返回 value

unref 的本质是一个条件解包器,它对 ref 进行解包,对普通值透明传递。

2. 使用场景

unref 常用于编写通用函数,这些函数需要同时处理 ref 和普通值:

js
// 一个接受数字或 ref<number> 的函数
function double(num) {
  const rawValue = unref(num);
  return rawValue * 2;
}

const count = ref(5);
console.log(double(count)); // 10
console.log(double(3));     // 6

computedwatch 中也常见:

js
const doubled = computed(() => unref(count) * 2);

3. 与 proxyRefs 的关系

unrefproxyRefsget 拦截器的核心逻辑。proxyRefs 在读取属性时,内部就调用了类似 unref 的逻辑来实现自动解包。


三、toRaw:直达原始对象的通道

toRaw 是更强大的“逃生舱”,它能穿透 reactivereadonly 创建的 Proxy,返回其背后的原始对象。

1. 实现原理

toRaw 的实现依赖于在创建 Proxy 时建立的反向引用:

ts
function reactive(target) {
  // ... 其他逻辑
  
  const observed = new Proxy(target, mutableHandlers);

  // 关键:在原始对象上设置 __v_raw 属性,指向代理
  def(target, '__v_raw', observed);
  // 在代理对象上设置 __v_raw 属性,指向原始对象
  def(observed, '__v_raw', target);

  return observed;
}

toRaw 函数的实现:

ts
function toRaw(observed) {
  // 如果传入的是代理对象,返回其 __v_raw(即原始对象)
  // 如果传入的是原始对象,__v_raw 可能不存在或指向自身
  const raw = observed && observed.__v_raw;
  return raw ? toRaw(raw) : observed;
}

详细解析

  • observed.__v_raw 直接指向创建 Proxy 时的原始 target
  • 函数使用递归确保即使传入的是嵌套的代理,也能最终拿到最原始的对象。
  • 如果传入的是普通对象(无 __v_raw),则直接返回。

2. 为什么需要 __v_raw 反向引用?

Proxy 本身不提供直接访问目标对象的 API。__v_raw 是 Vue 主动建立的桥梁,使得 toRaw 能够可靠地获取原始对象。


四、toRaw 的必须使用场景

尽管应尽量避免使用 toRaw(因为它绕过了响应式系统),但在以下场景中是必要的:

1. 避免不必要的响应式开销

当需要对大型或深层对象进行复杂计算时,持续的依赖追踪会带来性能负担。

js
const state = reactive({
  items: largeArray, // 包含数千个对象
});

// ❌ 错误:每次迭代都会触发 track,性能极差
function processItems() {
  return state.items.map(item => heavyComputation(item));
}

// ✅ 正确:使用 toRaw 避免响应式开销
function processItems() {
  const rawItems = toRaw(state.items);
  return rawItems.map(item => heavyComputation(item));
}

2. 与不兼容 Proxy 的第三方库集成

许多库(如 moment.jslodash、某些图表库)在内部使用 Object.keysfor...in 或直接属性访问,可能无法正确处理 Proxy

js
import { someChartLibrary } from 'chart-lib';

const chartData = reactive({
  labels: ['A', 'B', 'C'],
  datasets: [/* ... */]
});

// ❌ 可能失败:图表库无法正确读取代理对象
someChartLibrary.render(toRaw(chartData));

// ✅ 安全:传递原始对象
someChartLibrary.render(toRaw(chartData));

3. 对象身份比较

=== 比较代理对象和原始对象会失败。

js
const original = { user: 'John' };
const proxy = reactive(original);

console.log(proxy === original); // false

// 在缓存或集合中,身份很重要
const cache = new WeakMap();
cache.set(original, 'cached data');

// ❌ 失败:proxy !== original
console.log(cache.get(proxy)); // undefined

// ✅ 正确:先转为原始对象
console.log(cache.get(toRaw(proxy))); // 'cached data'

4. 序列化与持久化

JSON.stringify 无法正确处理 Proxy

js
const state = reactive({ count: 1 });

// ❌ 输出 "{}",因为 Proxy 的 enumerable 属性有限
console.log(JSON.stringify(state));

// ✅ 正确:序列化原始对象
console.log(JSON.stringify(toRaw(state))); // {"count":1}

// 保存到 localStorage
localStorage.setItem('state', JSON.stringify(toRaw(state)));

5. 绕过响应式限制

某些操作(如添加不可枚举属性、修改原型)在 Proxy 下可能被阻止或行为异常。

js
const obj = reactive({});
// ❌ 可能无效或警告
Object.defineProperty(obj, 'hidden', { value: 'secret', enumerable: false });

// ✅ 直接操作原始对象
const raw = toRaw(obj);
Object.defineProperty(raw, 'hidden', { value: 'secret', enumerable: false });

五、重要警告与最佳实践

  1. 不要修改 toRaw 返回的对象

    js
    const raw = toRaw(state);
    raw.count = 10; // ❌ 危险!绕过了响应式系统

    直接修改原始对象不会触发视图更新。如果必须修改,应在响应式上下文中进行。

  2. 仅在必要时使用toRaw 是“逃生舱”,不应作为常规操作手段。滥用会破坏响应式数据流。

  3. 理解副作用: 使用 toRaw 后,你获得的是一个“死”数据快照。后续对响应式对象的修改不会反映在这个快照上。

  4. 与 ref 的关系toRawref 无效。要获取 ref 的原始值,应使用 .valueunref


六、总结

unreftoRaw 是 Vue 3 响应式系统的“逃生舱”:

  • unref:一个简单的条件解包器,对 ref 返回 .value,对其他值透明传递。适用于需要统一处理 ref 和普通值的通用函数。
  • toRaw:通过 __v_raw 私有标志,穿透 Proxy 返回原始对象。必须用于:
    • 性能敏感的复杂计算。
    • 集成不兼容 Proxy 的第三方库。
    • 对象身份比较(WeakMapSet)。
    • 序列化(JSON.stringify)。
    • 绕过响应式限制的操作。

理解这些函数的原理和适用场景,能让开发者在享受响应式便利的同时,也能在必要时安全地“逃逸”到原始数据层面,构建更高效、更兼容的应用程序。