引言

在使用 Vue 3 开发过程中,我遇到了一个令人困惑的问题:在子组件中使用 props.node 查找对象索引时总是返回 -1,但使用 toRaw(props.node) 却能正常工作。这背后的原因是什么?让我们深入探索 Vue 3 的响应式系统。

问题重现

场景描述

在开发一个多视角编辑器时,我需要管理多个视角节点:

// 父组件
const viewNodePanels = reactive<ViewNodeEditor[]>([]);

// 子组件中查找索引
const index = parentSystem.getViewNodeIndex(props.node); // 返回 -1
const index = parentSystem.getViewNodeIndex(toRaw(props.node)); // 返回正确索引

Vue 3 响应式系统揭秘

从 Vue 2 到 Vue 3 的演进

Vue 2 的响应式原理:

  • 使用 Object.defineProperty 劫持属性

  • 递归遍历对象的所有属性

  • 数组方法重写

Vue 3 的响应式原理:

  • 使用 Proxy 代理整个对象

  • 惰性劫持,按需触发

  • 更好的性能和更完整的功能

Proxy 代理机制

// 简单的 Proxy 示例
const rawObject = { name: "原始对象", value: 42 };
const proxyObject = new Proxy(rawObject, {
    get(target, key) {
        console.log(`读取属性: ${key}`);
        return target[key];
    },
    set(target, key, value) {
        console.log(`设置属性: ${key} = ${value}`);
        target[key] = value;
        return true;
    }
});

console.log(proxyObject === rawObject); // false
console.log(toRaw(proxyObject) === rawObject); // true

问题根源分析

严格相等比较的陷阱

class ViewMultiNodeEditor {
    private _viewNodes: ViewNodeEditor[] = []; // 存储原始对象
    
    getViewNodeIndex(node: ViewNodeEditor): number {
        // 这里进行的是严格相等比较
        return this._viewNodes.indexOf(node);
    }
}

// 实际使用场景
const rawNode = new ViewNodeEditor();
const reactiveNodes = reactive([rawNode]);

// 在组件中
const props = defineProps<{ node: ViewNodeEditor }>();

// 问题发生:
console.log(props.node === rawNode); // false - Proxy ≠ 原始对象
console.log(toRaw(props.node) === rawNode); // true - 原始对象相等

为什么 Vue 3 要使用 Proxy?

  1. 完整的对象操作支持

    • Proxy 可以拦截所有对象操作

    • 包括属性添加、删除、in 操作符等

  2. 更好的性能

    • 惰性劫持,不需要递归遍历

    • 按需创建响应式依赖

  3. 数组处理的改进

    • 不需要重写数组方法

    • 直接支持索引操作和 length 变化

实战解决方案

方案 1:使用 toRaw() 获取原始对象

import { toRaw } from 'vue';

// 在与原生系统交互时使用原始对象
const handleOperation = () => {
    const rawNode = toRaw(props.node);
    const index = parentSystem.getViewNodeIndex(rawNode); // 正常工作
    
    // 或者直接操作原始对象
    rawNode.someNativeMethod();
};

方案 2:统一数据源管理

// 如果频繁需要原始对象,考虑存储原始引用
const rawData = {
    nodes: [] as ViewNodeEditor[]
};

// 响应式包装
const reactiveData = reactive(rawData);

// 需要原始对象时直接访问
const getNodeIndex = (node: ViewNodeEditor) => {
    return rawData.nodes.indexOf(toRaw(node));
};

方案 3:自定义查找逻辑

// 在系统类中添加支持代理对象的查找方法
class ViewMultiNodeEditor {
    private _viewNodes: ViewNodeEditor[] = [];
    
    // 原始查找方法
    getViewNodeIndex(node: ViewNodeEditor): number {
        return this._viewNodes.indexOf(node);
    }
    
    // 支持代理对象的查找方法
    getViewNodeIndexWithProxy(proxyNode: ViewNodeEditor): number {
        const rawNode = toRaw(proxyNode);
        return this._viewNodes.indexOf(rawNode);
    }
    
    // 或者使用唯一标识符查找
    getViewNodeIndexById(uuid: string): number {
        return this._viewNodes.findIndex(node => node.uuid === uuid);
    }
}

什么时候需要使用 toRaw()

需要原始对象的场景

  1. 与第三方库交互

    // 传递给非 Vue 系统
    thirdPartyLibrary.processObject(toRaw(props.data));
  2. 性能敏感操作

    // 大数据量操作
    const rawData = toRaw(largeDataSet);
    performHeavyComputation(rawData);
  3. 对象比较和查找

    // 在原始数组中查找
    const index = originalArray.indexOf(toRaw(proxyObject));
  4. 序列化

    // 避免代理对象被序列化
    const serialized = JSON.stringify(toRaw(state));

不需要 toRaw() 的场景

  1. 模板渲染

    <template>
      <div>{{ node.name }}</div> <!-- 直接使用代理 -->
    </template>

  2. 计算属性

    const displayName = computed(() => props.node.name);

  3. Vue 生态系统内的方法调用

    // Vue 组件内的方法
    const handleClick = () => {
      props.node.updateName('新名称'); // 代理会自动处理
    };

最佳实践建议

1. 明确数据流向

// 清晰的注释说明数据用途
interface Props {
    node: ViewNodeEditor; // 用于模板渲染和 Vue 系统内部
}

// 需要与原生系统交互时转换为原始对象
const rawNode = toRaw(props.node); // 用于索引查找和原生方法调用

2. 统一对象标识管理

// 使用唯一标识符避免对象比较问题
class ViewNodeEditor {
    public readonly uuid: string = generateUUID();
    
    // 提供基于标识符的比较方法
    equals(other: ViewNodeEditor): boolean {
        return this.uuid === other.uuid;
    }
}

// 查找时使用标识符
const index = nodes.findIndex(node => node.uuid === targetNode.uuid);

3. 封装工具函数

// 创建工具函数处理代理对象
export function findIndexWithProxy<T>(
    array: T[], 
    proxyItem: T
): number {
    const rawItem = toRaw(proxyItem);
    return array.indexOf(rawItem);
}

// 使用
const index = findIndexWithProxy(originalArray, proxyObject);

总结

Vue 3 的 Proxy 代理机制是其响应式系统的核心,它提供了更好的性能和更完整的功能。然而,这种机制也带来了对象标识的变化:

  • Proxy 代理对象 ≠ 原始对象

  • 严格相等比较 (===) 在代理和原始对象之间会失败

  • toRaw() 是连接响应式世界和原生世界的桥梁

理解这个机制后,我们就能:

  • 正确地在需要时使用 toRaw()

  • 避免不必要的对象转换

  • 设计更健壮的数据流

  • 写出更可维护的 Vue 3 代码

记住:在模板和 Vue 生态内使用代理对象,在与原生系统交互时使用原始对象。这样的区分能让你的应用既享受响应式的好处,又能与各种原生系统无缝集成。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐