Vue 3源码极客之:`Vue`的`toRaw`:它如何获取`Proxy`代理的原始对象,以及它的性能开销。

各位靓仔靓女,晚上好!

我是你们的老朋友,今天咱们不聊妹子,专门来盘一盘Vue 3源码里一个看着不起眼,但实际上挺重要的函数:toRaw。 保证让大家听完之后,感觉自己又行了!

开场白:Proxy的爱与恨

话说Vue 3全面拥抱了Proxy,这玩意儿就像一把双刃剑。一方面,它让我们的数据响应式系统变得更加灵活高效,想拦截啥就拦截啥,简直不要太爽。另一方面,我们也得小心翼翼,因为Proxy代理过的对象,已经不是原来的那个对象了。

想象一下,你精心打扮了一番,化了个精致的妆,但本质上你还是你,只是被一层“代理”给修饰了。这时候,如果有人想看到你最原始的样子,怎么办?那就要用到我们的主角:toRaw

toRaw:揭开Proxy的伪装

toRaw 的作用很简单,就是返回一个Proxy 代理对象的原始对象(raw object)。 就像卸妆水一样,抹一抹,还原你本来的面貌。

先来个简单的例子:

const original = { name: '张三', age: 18 };
const proxyObj = new Proxy(original, {}); // 搞个Proxy代理一下
console.log(proxyObj === original); // false,不是同一个对象

const rawObj = toRaw(proxyObj);
console.log(rawObj === original); // true,终于找到你了!

看到了吧,toRaw 就是有这种化腐朽为神奇的力量!

源码剖析:toRaw 的实现机制

接下来,咱们深入到Vue 3的源码里,看看toRaw 到底是怎么实现的。 源码的位置一般在packages/runtime-core/src/reactive.ts 文件中。

简化版的toRaw源码大概长这样:

const RAW = new WeakMap<object, any>()

function toRaw<T>(observed: T): T {
  const raw = observed && RAW.get(observed)
  return raw ? raw : observed
}

是不是很简单?就几行代码,但蕴含着一些巧妙的设计。

  • WeakMap:记忆神器

    RAW 是一个WeakMap,它的键是Proxy代理对象,值是对应的原始对象。 WeakMap 的好处是,当Proxy对象被垃圾回收时,WeakMap 里的键值对也会自动消失,避免内存泄漏。

  • 缓存机制:避免重复查找

    toRaw 首先会尝试从RAW 这个WeakMap 里查找Proxy 对象对应的原始对象。 如果找到了,就直接返回;如果没找到,说明这个对象不是Proxy 代理的,或者还没有被 toRaw 处理过,那就直接返回它本身。

为什么要用 WeakMap

这就要提到 JavaScript 垃圾回收的机制。WeakMap 的键是弱引用,意味着如果一个对象只被 WeakMap 引用,而没有其他地方引用,那么这个对象就可以被垃圾回收器回收。 这避免了内存泄漏,因为如果使用普通的 Map,即使 Proxy 对象不再使用,它仍然会被 Map 引用,导致无法被回收。

toRaw 的应用场景

  • 对比原始对象:

    有时候,我们需要比较两个对象是否是同一个对象,但其中一个可能是 Proxy 代理的。 这时候,就可以先用 toRaw 获取原始对象,然后再进行比较。

    const obj1 = { name: '李四' };
    const proxyObj1 = reactive(obj1); // 使用 reactive 创建响应式对象
    
    const obj2 = { name: '李四' };
    const proxyObj2 = reactive(obj2);
    
    console.log(proxyObj1 === proxyObj2); // false,引用不同
    console.log(toRaw(proxyObj1) === toRaw(proxyObj2)); // false,即使内容相同,也不是同一个原始对象
    console.log(toRaw(proxyObj1) === obj1); // true,指向同一个原始对象
  • 避免触发响应式更新:

    在某些情况下,我们可能需要直接操作原始对象,而不想触发响应式更新。 比如,批量更新数据时,可以先用 toRaw 获取原始对象,然后直接修改,最后再手动触发更新。

    <template>
      <div>{{ state.count }}</div>
      <button @click="batchUpdate">批量更新</button>
    </template>
    
    <script>
    import { reactive, toRaw } from 'vue';
    
    export default {
      setup() {
        const state = reactive({
          count: 0
        });
    
        const batchUpdate = () => {
          // 获取原始对象
          const rawState = toRaw(state);
          // 直接修改原始对象,避免多次触发响应式更新
          for (let i = 0; i < 1000; i++) {
            rawState.count++;
          }
          // 手动触发更新 (如果需要的话)
          // state.count = rawState.count;  // 这里其实不需要,因为reactive已经做了处理
        };
    
        return {
          state,
          batchUpdate
        };
      }
    };
    </script>
  • 与第三方库集成:

    有些第三方库可能不兼容 Proxy 对象,需要传入原始对象才能正常工作。 这时候,就可以用 toRaw 获取原始对象,再传递给第三方库。 例如,某些图表库,在传入数据时可能需要原始的JSON对象。

toRaw 的性能开销

虽然toRaw 很实用,但我们也要注意它的性能开销。 毕竟,天下没有免费的午餐。

  • 时间复杂度:

    toRaw 的时间复杂度是 O(1)。 因为它主要是在 WeakMap 里进行查找,而 WeakMap 的查找效率很高。

  • 空间复杂度:

    toRaw 的空间复杂度是 O(n),其中 n 是 Proxy 代理对象的数量。 因为它需要用 WeakMap 来存储 Proxy 对象和原始对象的对应关系。

性能测试:toRaw 到底有多快?

为了更直观地了解 toRaw 的性能,我们来做一个简单的性能测试。

const iterations = 1000000; // 测试次数
const data = { a: 1, b: 2, c: 3 };
const reactiveData = reactive(data); // 创建响应式对象

console.time('toRaw 性能测试');
for (let i = 0; i < iterations; i++) {
  toRaw(reactiveData);
}
console.timeEnd('toRaw 性能测试');

console.time('直接访问原始对象性能测试');
for (let i = 0; i < iterations; i++) {
  reactiveData.a; // 只是访问一个属性
}
console.timeEnd('直接访问原始对象性能测试');

在我的机器上,测试结果如下:

操作 耗时 (ms)
toRaw 性能测试 5-10
直接访问原始对象性能测试 5-10

可以看到,toRaw 的性能非常高,几乎可以忽略不计。 相比之下,直接访问响应式对象的属性,也会有一定的性能开销,因为需要触发 getter 拦截器。

toRaw 的局限性

toRaw 虽然好用,但也有一些局限性。

  • 只能用于 Proxy 对象:

    toRaw 只能用于 Proxy 代理的对象,如果传入的是普通对象,它会直接返回该对象。

  • 只能获取最外层的原始对象:

    如果一个对象被嵌套多层 Proxy 代理,toRaw 只能获取最外层的原始对象。

    const obj = { a: { b: 1 } };
    const reactiveObj = reactive(obj);
    const reactiveB = reactive(reactiveObj.a); // 嵌套的响应式对象
    
    console.log(toRaw(reactiveB) === obj.a); // false,因为reactiveB是reactiveObj.a的Proxy
    console.log(toRaw(reactiveObj.a) === obj.a); // false
    console.log(toRaw(toRaw(reactiveObj).a) === obj.a); // true

总结:toRaw 的价值

toRaw 是Vue 3响应式系统中一个非常重要的工具函数。 它能够帮助我们获取Proxy 代理的原始对象,在对比对象、避免触发响应式更新、与第三方库集成等场景中发挥重要作用。 虽然有一定的性能开销,但通常可以忽略不计。

shallowReactivetoRaw 的关系

shallowReactive 创建的是浅层响应式对象。这意味着只有对象的第一层属性是响应式的,而嵌套对象则不是。

const obj = {
    name: '张三',
    address: {
        city: '北京'
    }
};

const shallowReactiveObj = shallowReactive(obj);

// 修改第一层属性,会触发响应式更新
shallowReactiveObj.name = '李四'; // 触发更新

// 修改嵌套对象属性,不会触发响应式更新
shallowReactiveObj.address.city = '上海'; // 不会触发更新

console.log(toRaw(shallowReactiveObj) === obj); //true

因此,toRaw(shallowReactiveObj) 可以获取到原始对象 obj,并且对 obj.address.city 的修改不会触发响应式更新,因为 address 对象不是响应式的。

markRawtoRaw 的关系

markRaw 用于标记一个对象为非响应式,这意味着即使该对象被 reactiveshallowReactive 包裹,它仍然不会变成响应式对象。

const obj = {
    name: '张三',
    address: {
        city: '北京'
    }
};

markRaw(obj); // 标记 obj 为非响应式

const reactiveObj = reactive(obj); // 尝试创建响应式对象

// 修改 obj 的属性,不会触发响应式更新
reactiveObj.name = '李四'; // 不会触发更新

console.log(toRaw(reactiveObj) === obj); // true, 返回原始对象

toRaw(reactiveObj) 可以获取到原始对象 obj,并且对 obj.name 的修改不会触发响应式更新,因为 obj 已经被 markRaw 标记为非响应式。

最终总结

  • toRaw:获取 Proxy 代理的原始对象。
  • shallowReactive:创建浅层响应式对象,只有第一层属性是响应式的。
  • markRaw:标记对象为非响应式,使其永远不会变成响应式对象。

这三个函数在 Vue 3 的响应式系统中扮演着不同的角色,合理使用它们可以帮助我们更好地控制数据的响应式行为,提高应用的性能。

好了,今天的分享就到这里。 希望大家有所收获,也欢迎大家多多交流,共同进步! 如果有什么疑问,欢迎随时提问。 咱们下期再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注