前端难点: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 变量与滚动状态类名》
相关推荐
支支დ3 小时前
VO by Vercel 前端特定优势:为什么它是构建 AI 应用的新范式
前端·人工智能
香芋芋圆4 小时前
AI 冲击内卷之下,普通前端如何破局?WebGIS—— 低门槛突围赛道
前端·javascript·人工智能·学习·职场发展
INS_KF4 小时前
【编程笔记】成员函数中两个 const 的区别(const Data &getData() const;)
前端·javascript·笔记
宿6745 小时前
vue3-async
前端·javascript·vue.js
YXWik65 小时前
记录前端请求接口在浏览器请求响应的Preview和Response展示的一样的问题
前端
2501_928996225 小时前
GPT-4o换DeepSeek迁移成本多少?中科热备解析API聚合平台技术账本
前端·数据库·人工智能
东风破_5 小时前
从跨域到 WebSocket:前端跨域方案、SSE 与双向实时通信详解
前端·后端
原则猫5 小时前
TS 类型工具
前端
excel6 小时前
Nuxt + Twin CSS 中宽度超过屏幕时底部出现空白的原因与解决方案
前端·javascript
计算机魔术师7 小时前
美国司法部正式站队 OpenAI,AI 训练的版权账要这么算了
前端