在 Vue 3 的响应式系统中,toRaw 就像一把"去毒手术刀"------它能把被 Proxy 包装的对象还原为原始 JavaScript 对象。但你可能发现,官方文档对它的使用时机语焉不详,"需要原始对象时"的提示让人更困惑了。
核心规律其实很简单:当你需要"对象身份认同"(Object Identity)时,就必须使用 toRaw。 本文结合 Babylon.js 3D 引擎开发中的实际案例,帮你建立清晰的使用直觉。
一、理解对象身份危机
Vue 3 使用 Proxy 实现响应式,这意味着:
TypeScript
import { reactive } from 'vue'
import { Mesh } from '@babylonjs/core'
const box = reactive(new Mesh("box", scene))
console.log(box === scene.getMeshByName("box")) // false!
reactive() 创建的是原对象的代理副本 ,二者在内存中地址不同。这会导致依赖 === 或对象作为 Key 的场景全部失效。
二、必须使用 toRaw 的三大场景
场景一:3D 对象缓存管理(Map/WeakMap)
在 Babylon.js 中,我们经常需要用 Map 存储 mesh 的附加元数据(如选中状态、业务属性):
TypeScript
import { reactive, toRaw } from 'vue'
import { Mesh } from '@babylonjs/core'
// 错误示例:响应式对象直接作为 Key
const meshMetaMap = new Map()
const selectedMeshes = reactive(new Set())
function selectMesh(mesh) {
// ❌ 陷阱:mesh 可能是 reactive 包装过的
selectedMeshes.add(mesh)
// 后续查找失败!因为 reactive(mesh) !== mesh
console.log(selectedMeshes.has(mesh)) // 可能返回 false
}
// 正确做法:统一使用 toRaw 确保身份一致
function selectMeshSafe(mesh) {
const rawMesh = toRaw(mesh)
selectedMeshes.add(rawMesh)
// 现在一定能找到,因为都是原始对象地址
console.log(selectedMeshes.has(rawMesh)) // true
}
// 更实际的案例:自定义拾取缓存
const pickableCache = new WeakMap()
function onScenePointerDown(mesh) {
// 必须用原始对象作为 WeakMap 的 key
const rawMesh = toRaw(mesh)
if (!pickableCache.has(rawMesh)) {
pickableCache.set(rawMesh, {
originalColor: rawMesh.material?.diffuseColor?.clone(),
clickCount: 0
})
}
const meta = pickableCache.get(rawMesh)
meta.clickCount++
// 高亮效果
rawMesh.renderOutline = true
}
为什么重要? Babylon.js 内部大量使用 WeakMap 缓存(如材质、几何体),当你把响应式 mesh 传给引擎 API 时,引擎认不出这是"老熟人",会重复创建资源导致内存泄漏。
场景二:材质与纹理的共享判定
Babylon.js 中,相同材质应该共享实例以避免显存浪费。判断材质是否已存在时,需要严格相等比较:
TypeScript
import { reactive, toRaw } from 'vue'
import { StandardMaterial, Texture } from '@babylonjs/core'
const materialLibrary = reactive(new Map()) // name -> material
function getSharedMaterial(name, scene, reactiveOptions) {
// ❌ 错误:如果 reactiveOptions 是响应式的,以下判断永远失败
const existing = materialLibrary.get(name)
if (existing && existing.options === reactiveOptions) {
return existing.material
}
// ✅ 正确:比较原始对象
const rawOptions = toRaw(reactiveOptions)
// 检查是否有配置完全相同的材质
for (const [_, item] of materialLibrary) {
if (shallowEqual(toRaw(item.options), rawOptions)) {
return item.material // 复用已有材质
}
}
// 创建新材质
const mat = new StandardMaterial(name, scene)
applyOptions(mat, rawOptions) // 应用原始配置,避免触发 Vue 追踪
materialLibrary.set(name, {
material: mat,
options: rawOptions // 存储原始对象,而非响应式代理
})
return mat
}
性能影响: 如果不使用 toRaw,每次比较 options === otherOptions 都会因 Proxy 包装而返回 false,导致相同材质被重复创建数十次,迅速耗尽 GPU 显存。
场景三:高频顶点计算与序列化
处理地形生成或物理模拟时,你可能需要直接操作顶点数据:
TypeScript
import { reactive, toRaw } from 'vue'
import { MeshBuilder, Vector3 } from '@babylonjs/core'
const terrainState = reactive({
vertices: [], // 10000+ 个 Vector3
isDirty: false
})
function updateTerrainHeights(heightMap) {
// ❌ 性能灾难:每次访问顶点都触发 Proxy 拦截
const start = performance.now()
terrainState.vertices.forEach((v, i) => {
v.y = heightMap[i] // 触发 setter,Vue 尝试追踪变化
})
console.log(performance.now() - start) // 约 150ms
// ✅ 极速模式:脱离响应式系统
const rawVertices = toRaw(terrainState.vertices)
rawVertices.forEach((v, i) => {
const rawV = toRaw(v) // Vector3 也可能被深层代理
rawV.y = heightMap[i] // 纯 JS 操作,无追踪开销
})
console.log(performance.now() - start) // 约 5ms
// 手动标记状态更新
terrainState.isDirty = true
}
关键细节: Babylon.js 的 Vector3、Color4 等数学对象如果被包裹在 reactive 中,每次 .x、.y 访问都会触发依赖收集。在渲染循环(60fps)中,这会导致严重的帧率下降。
三、坚决不要使用 toRaw 的场景
场景一:模板渲染与计算属性
TypeScript
<script setup>
import { reactive, computed, toRaw } from 'vue'
import { Mesh } from '@babylonjs/core'
const mesh = reactive(new Mesh("hero", scene))
// ❌ 错误:返回原始对象,模板失去响应性
const position = computed(() => toRaw(mesh).position)
// ✅ 正确:保持响应式连接
const position = computed(() => mesh.position)
</script>
<template>
<!-- 不需要 toRaw,Vue 自动解包 -->
<div>Mesh X: {{ mesh.position.x }}</div>
<!-- ❌ 多余且有害:破坏响应性 -->
<div>Mesh X: {{ toRaw(mesh).position.x }}</div>
</template>
场景二:组件 Props 传递
当你把 Babylon 对象传给子组件时,不要 在传递前 toRaw:
TypeScript
// 父组件
const box = reactive(new Mesh("box", scene))
// ❌ 错误:子组件收到的是死数据,无法追踪变化
<MeshInspector :target="toRaw(box)" />
// ✅ 正确:子组件通过 props 接收,内部自行决定是否需要 toRaw
<MeshInspector :target="box" />
子组件内部如果需要进行身份比较(如加入 Set),再使用 toRaw(props.target)。
场景三:JSON 序列化
很多人误以为 JSON.stringify 前需要 toRaw,其实完全不需要:
TypeScript
const state = reactive({
meshData: { name: "box", position: { x: 1, y: 2 } }
})
// ✅ 直接序列化即可,结果正确
const json = JSON.stringify(state)
// 结果: {"meshData":{"name":"box","position":{"x":1,"y":2}}}
// ❌ 多余操作
const json = JSON.stringify(toRaw(state)) // 结果完全一样
因为 JSON.stringify 会递归访问属性,而属性值本身就是原始值(除非嵌套 reactive,但那也会被正常序列化)。
四、实战案例:3D 场景管理器
下面是一个完整的 Vue 3 + Babylon.js 示例,展示如何正确使用 toRaw:
TypeScript
<script setup>
import { ref, reactive, onMounted, toRaw } from 'vue'
import { Engine, Scene, MeshBuilder, StandardMaterial, Color3 } from '@babylonjs/core'
const canvasRef = ref(null)
const selectedMeshes = reactive(new Set())
const meshStore = reactive(new Map()) // id -> mesh
let scene = null
onMounted(() => {
const engine = new Engine(canvasRef.value, true)
scene = new Scene(engine)
// 创建网格
const box = MeshBuilder.CreateBox("box", {}, scene)
const sphere = MeshBuilder.CreateSphere("sphere", {}, scene)
// 存储原始对象(不是响应式对象)
meshStore.set("box", toRaw(box))
meshStore.set("sphere", toRaw(sphere))
// 渲染循环
engine.runRenderLoop(() => {
scene.render()
})
// 点击事件
scene.onPointerDown = (evt, pickInfo) => {
if (pickInfo.hit) {
toggleSelection(pickInfo.pickedMesh)
}
}
})
function toggleSelection(mesh) {
// 关键:确保使用原始对象作为 Set 的 key
const rawMesh = toRaw(mesh)
if (selectedMeshes.has(rawMesh)) {
selectedMeshes.delete(rawMesh)
rawMesh.renderOutline = false
rawMesh.material = getMaterial("default") // 复用材质
} else {
selectedMeshes.add(rawMesh)
rawMesh.renderOutline = true
rawMesh.outlineColor = Color3.Red()
rawMesh.outlineWidth = 0.1
}
}
// 材质缓存系统
const materialCache = new Map()
function getMaterial(type) {
// 缓存 key 直接比较,必须保证对象身份
if (materialCache.has(type)) {
return materialCache.get(type)
}
const mat = new StandardMaterial(type, scene)
mat.diffuseColor = type === "default" ? Color3.Gray() : Color3.Blue()
materialCache.set(type, mat)
return mat
}
// 批量操作:性能敏感场景
function randomizePositions() {
// 转换为数组时 toRaw,避免响应式开销
const meshes = Array.from(meshStore.values()).map(toRaw)
meshes.forEach(mesh => {
mesh.position.x = Math.random() * 10
mesh.position.y = Math.random() * 10
// 直接修改,不触发 Vue 追踪,提升帧率
})
}
</script>
<template>
<div>
<canvas ref="canvasRef" style="width: 800px; height: 600px;" />
<div>已选中: {{ selectedMeshes.size }} 个对象</div>
<button @click="randomizePositions">随机位置</button>
</div>
</template>
五、决策流程图
是否在 Vue 模板/计算属性中使用?
├── 是 → ❌ 不要使用 toRaw
└── 否 → 数据是否离开 Vue 系统?(传给 Babylon.js/Map/Set/Worker)
├── 否 → ❌ 不需要 toRaw
└── 是 → 是否需要对象身份识别?(作为 Key、=== 比较、缓存)
├── 是 → ✅ 必须使用 toRaw
└── 否 → 是否高频性能敏感操作?
├── 是 → ✅ 推荐使用 toRaw
└── 否 → 可用可不用(一般不用)
结语
toRaw 的本质是对象身份的还原剂 。在 Babylon.js 这类依赖大量对象引用缓存和严格相等判断的图形引擎中,它是避免内存泄漏和逻辑混乱的必备工具。记住:只有在跨越 Vue 响应式边界、进入外部生态系统时,才需要考虑这把"手术刀"。在 Vue 的领地内,让它保持响应式才是最好的选择。