前端难点: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 变量与滚动状态类名》
相关推荐
子兮曰3 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰3 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万3 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝3 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
A黄俊辉A3 天前
uniapp webview中实现 app和内嵌的H5双向通信
vue.js·json
三十而立洋3 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
卡布鲁3 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
honkun63 天前
vue 表格组件 vxe-table 配置 ajax 请求自动加载数据与表单查询
vue.js·vxe-table
kybs19913 天前
全球灾害数据分析可视化 毕业设计-附源码66794
vue.js·spring boot·mysql·安全·django·c#·asp.net
汉堡大王95273 天前
Jev:不是聊天机器人, 而是一个智能 if 语句
前端·人工智能·后端