前端难点: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 下有成千上万个 Mesh、Material、Geometry 节点;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 变量与滚动状态类名》
相关推荐
2603_965896626 分钟前
Grid 网格布局|CSS 二维页面排版体系
前端·css
有味道的男人22 分钟前
搜好货平台关键词商品搜索接口:分页容错与商品数据轻量化提取方案
服务器·前端·javascript
lovingsoft1 小时前
Chrome Lighthouse 详解
前端·chrome
Qlly1 小时前
全栈工程师怎么接需求?从一句「加个功能」聊到能开发、能验收
前端·后端
马可家的菠萝1 小时前
你的微信“文件传输助手”,是不是已经成了第二个网盘?
前端·后端·架构
略略略啦1 小时前
Vibe Coding 基于Spec DD和TDD的实践指南
前端
shmily麻瓜小菜鸡2 小时前
前端实现图片与PDF文字识别敏感信息打码方案
前端·pdf·状态模式
打工仔折腾 AI2 小时前
LangChain 工具调用实战:从 Tool 定义到 Agent 执行链路拆解
java·前端·人工智能·后端·python·langchain·ai agent 实战
汉堡大王95272 小时前
单次输出拉到100万Token:拆完 Gemini 4 Argon 的续写协议,才看懂谷歌这7个月在憋什么
前端·人工智能·后端
看浪的路人2 小时前
第9讲:大模型应用监控与可观测性——OpenTelemetry + Prometheus + Grafana 全链路追踪
java·前端·spring