前端难点:Vue3 响应式遇上 Three.js / ECharts —— 为什么要用 shallowRef?

49 阅读3分钟

前端难点:Vue3 响应式遇上 Three.js / ECharts —— 为什么要用 shallowRef?

摘要:Three.js / ECharts 实例用 ref 包一层就卡顿、tooltip 异常?该用 shallowRef 和 markRaw。 专栏:前端难点实战 · 阅读约 7 分钟 · 难度:⭐⭐⭐⭐ 实战场景:Vue3 中大型 B 端后台


一、现象:图表 tooltip 异常、3D 场景卡顿、内存暴涨

在实际项目的 3D 模型查看器Echart 组件里,如果这样写:

// ❌ 危险写法
const scene = ref(new THREE.Scene())
const myChart = ref(echarts.init(dom))

可能出现:

  • ECharts tooltip 位置错乱、事件不触发
  • Three.js 渲染变慢(Vue 递归代理整个 scene graph)
  • 控制台警告 Reactive object detected in readonly operation

二、根因:ref() 会深度 reactive

Vue 3 的 ref() 对对象类型会调用 reactive()递归把对象所有属性变成 Proxy:

const obj = ref({ a: { b: { c: 1 } } })
// obj.value.a.b.c 也被代理

Three.js 的 Scene 下有成千上万个 MeshMaterialGeometry 节点;ECharts 实例内部同样有复杂对象树。被 Vue 代理后:

  1. 性能:每次 render 触发大量 Proxy 读写
  2. 兼容性:库内部可能用 ===、私有字段、非 configurable 属性,与 Proxy 冲突
  3. 内存:响应式依赖追踪额外开销

三、解法:shallowRef + markRaw

3.1 shallowRef:只代理 .value 引用

const scene = shallowRef<THREE.Scene | null>(null)
const myChart = shallowRef<echarts.ECharts | null>(null)

scene.value = new THREE.Scene()  // Scene 内部不会被代理

替换 .value 整对象时仍会触发更新;修改 scene.value.children 不会触发 Vue 重渲染——而这正是我们想要的(渲染由 Three 的 rAF 循环驱动)。

3.2 markRaw:永久标记为非响应式

myChart.value = markRaw(echarts.init(chartDom.value, 'normal'))

即使不小心放进 reactive 对象,也不会被代理。


四、实际项目中的分层实践

数据类型推荐 API示例
Three Scene/Camera/RenderershallowRef3D 模型查看器
ECharts 实例shallowRef + markRawEchart 组件
UI 状态 loading/tooltiprefloading.value = true
模型 mesh 列表(只读展示)shallowRefmodelData
MQTT 消息 payloadref / 普通对象业务数据
// 3D 查看器组件节选
const scene = shallowRef<THREE.Scene | null>(null)
const hoverData = shallowRef<HoverEventData | null>(null)  // 含 Mesh 引用
const loading = ref(true)  // 纯 UI 布尔值用 ref

五、什么时候必须用 ref?

  • 模板里要绑定的简单 UI 状态
  • 需要 deep watch 的表单 model(或用 reactive
  • 数组/map 增删要驱动列表重渲染

不要把整个第三方实例塞进 reactive 表单对象。


六、组件卸载时的清理(另一个难点)

响应式解绑 ≠ GPU/内存释放。Three.js / ECharts 必须手动 dispose:

onUnmounted(() => {
  myChart.value?.dispose()
  renderer.value?.dispose()
  renderer.value?.forceContextLoss()
  controls.value?.dispose()
})

Vue 组件销毁不会帮你清理 WebGL context。


七、小结

问题方案
重型对象被深度代理shallowRef
实例需永久免代理markRaw
渲染循环交给 Three/ECharts,不依赖 Vue 重渲染
内存泄漏onUnmounted 里 dispose

写在最后

以上难点来自真实 B 端项目工程实践。若对你有帮助,欢迎 点赞、收藏,有问题评论区交流。

发布到掘金时建议

  • 分类:前端
  • 标签:前端、Vue.js、Three.js、ECharts、JavaScript
  • 封面:DevTools 布局截图或代码片段图
  • 摘要:复制文首「摘要」段落到编辑器摘要栏

专栏「前端难点实战」

  • 上一篇:《前端难点:Flex 布局里「表格/页面撑不满」—— min-height: 0 到底解决什么?》
  • 下一篇:《前端难点:Element Plus 样式覆盖 —— :deep()、CSS 变量与滚动状态类名》