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

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

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


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

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

typescript 复制代码
// ❌ 危险写法
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:

typescript 复制代码
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 引用

typescript 复制代码
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:永久标记为非响应式

typescript 复制代码
myChart.value = markRaw(echarts.init(chartDom.value, 'normal'))

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


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

数据类型 推荐 API 示例
Three Scene/Camera/Renderer shallowRef 3D 模型查看器
ECharts 实例 shallowRef + markRaw Echart 组件
UI 状态 loading/tooltip ref loading.value = true
模型 mesh 列表(只读展示) shallowRef modelData
MQTT 消息 payload ref / 普通对象 业务数据
typescript 复制代码
// 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:

typescript 复制代码
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 变量与滚动状态类名》
相关推荐
梦曦i1 小时前
Vite插件开发框架:14个实用插件与完整工具包
前端
KaMeidebaby1 小时前
卡梅德生物技术快报|biotin 生物素标记抗体全流程
前端·人工智能·算法·数据挖掘·数据分析
VitoChang1 小时前
前端也能快速入门后端! NestJS前台和后台的Auth认证
前端·后端
TheITSea1 小时前
一、React初体验:搭建、解析现代开发环境
前端·react.js·前端框架
盒马盒马1 小时前
Rust:String
java·前端·rust
程序猿阿伟1 小时前
《Chrome非必要服务的精细化关闭指南》
前端·chrome·php
belong_my_offer1 小时前
理解前端函数
前端
长空任鸟飞_阿康2 小时前
RAG 文档摄入全链路,从原理到生产落地
vue.js·人工智能·python
沐土Arvin2 小时前
中国省市区json数据
前端