为什么 markRaw 能修复 Vue 3 + ECharts 的 resize 报错

为什么 markRaw 能修复 Vue 3 + ECharts 的 resize 报错

问题现象

在 Vue 3 项目中,将 ECharts 实例存入 data() 后,图表首次渲染正常,但一旦触发 resize() 或拖动 dataZoom 滑块,就会抛出:

复制代码
Cannot read properties of undefined (reading 'type')
  at dataSample.reset (chunk-C7FS5BWW.js:1968)
  at ECharts2.resize

根本原因:Vue 3 的响应式 Proxy

Vue 3 响应式原理

Vue 3 使用 ES6 Proxy 实现响应式系统(替代 Vue 2 的 Object.defineProperty)。

当你在 data() 中返回一个对象时,Vue 会对整个对象递归地套上 Proxy 拦截层:

js 复制代码
data() {
  return {
    chart: null,   // 初始为 null,没有问题
    count: 0
  }
}

当你后续赋值:

js 复制代码
this.chart = echarts.init(el)

Vue 的响应式系统检测到 chart 属性被赋值,会立即对这个新值(ECharts 实例)执行 reactive() 处理,即对其套上 Proxy


Proxy 如何破坏 ECharts 内部逻辑

ECharts 实例是一个极其复杂的对象,内部拥有数百个属性和方法,以及大量内部状态引用。其中有一类关键逻辑:对象身份检测(identity check)

ECharts 源码中大量使用 === 引用相等判断、instanceof 类型判断、WeakMap 键名查找:

js 复制代码
// ECharts 内部伪代码示例
const coordSys = seriesModel.coordinateSystem;
//   coordSys 是通过 WeakMap 查找或对象引用绑定的

if (coordSys.type === 'cartesian2d') { ... }  // 崩溃点

问题在于 :ECharts 在初始化时,将 coordinateSystem(坐标系对象)绑定到 seriesModel 上,这个绑定是通过原始对象引用完成的。

加了 Proxy 之后:

复制代码
原始 seriesModel  ──→  Proxy(seriesModel)  ──→  Vue 跟踪层
                            ↑
                        this.chart 访问的是这个

当 ECharts 内部通过 WeakMap 或对象引用去查找 coordinateSystem 时,它用的是原始对象 作为 key,但 Vue 代理后,外部传进来的是 Proxy 包装对象 ,两者不是同一个引用,导致查找失败,返回 undefined

具体崩溃链路:

复制代码
this.chart.resize()           ← this.chart 是 Proxy 包装的 ECharts 实例
  → ECharts2.prototype.resize
  → updateMethods.update
  → scheduler.performDataProcessorTasks
  → dataSample.reset(seriesModel)
  → coordSys = seriesModel.coordinateSystem   ← undefined(WeakMap key 不匹配)
  → coordSys.type   ← 💥 TypeError

为什么首次渲染正常?

因为首次 setOption 是在赋值之后同步调用的,此时 ECharts 内部的完整初始化流程在同一个调用栈中完成,坐标系绑定、数据处理器注册等都是在原始对象上进行,Proxy 的干扰相对局限。

resize()异步触发 (window resize 事件回调),此时 Vue 的响应式追踪已经深度介入,ECharts 从内部调度器重新触发 update 流程时,各种内部对象引用已经被 Proxy 污染,无法正常还原 coordinateSystem


解决方案:markRaw

API 说明

markRaw 是 Vue 3 提供的官方 API,用于显式标记一个对象,使其永远不会被 Vue 的响应式系统转换为 Proxy

js 复制代码
import { markRaw } from 'vue'

const obj = markRaw({ ... })
// obj 上会被打上 __v_skip: true 标记
// Vue 的 reactive/ref 看到这个标记后,不再对其套 Proxy

在 ECharts 中的使用

js 复制代码
import { markRaw } from 'vue'
import echarts from '@/utils/echarts'

createChart() {
  const el = this.$refs.chart
  if (!el) return null
  
  this.chart = markRaw(echarts.init(el))  // ← 关键:用 markRaw 包裹
  return this.chart
}

markRaw 在 ECharts 实例上打了 __v_skip: true 标记。之后无论何时访问 this.chart(即便 chart 属性本身受 Vue 追踪),Vue 也不会对这个值再套 Proxy。

效果对比

场景 不用 markRaw 用 markRaw
this.chart 的实际值 Proxy(ECharts实例) ECharts实例(原始)
ECharts 内部 WeakMap 查找 key 不匹配,返回 undefined key 匹配,正常返回
resize() 时 coordinateSystem undefined → 崩溃 正常对象 → 正常运行
Vue 模板响应式追踪 会追踪(不必要) 不追踪(正确行为)

深入:__v_skip 标记机制

Vue 3 源码中,markRaw 的实现非常简单:

js 复制代码
// @vue/reactivity/src/reactive.ts
export function markRaw<T extends object>(value: T): Raw<T> {
  def(value, ReactiveFlags.SKIP, true)  // 即 __v_skip = true
  return value
}

reactive()ref() 内部,Vue 会先检查 __v_skip

js 复制代码
function createReactiveObject(target, ...) {
  // 如果对象被标记为 skip,直接返回原始对象
  if (target[ReactiveFlags.SKIP]) {
    return target
  }
  // 否则创建 Proxy ...
}

所以 markRaw 的本质是在对象上写入一个"免疫标记",让 Vue 的响应式代理工厂直接放行。


适用场景总结

以下类型的第三方对象存入 Vue data/ref 时,都应当使用 markRaw

对象类型 原因
ECharts 实例 内部依赖对象引用一致性,Proxy 会破坏 WeakMap 查找
D3.js 选择集 同上,内部大量使用原始 DOM 引用
Three.js 场景/相机 内部引用链复杂,Proxy 会导致渲染管线错误
WebSocket 实例 原生对象,不需要响应式追踪
Worker 实例 同上
大型只读配置对象 避免不必要的深度代理,提升性能

最佳实践

js 复制代码
data() {
  return {
    // ✅ 初始为 null 没问题,Vue 不会代理 null
    chart: null,
  }
},
methods: {
  createChart() {
    const instance = echarts.init(this.$refs.chart)
    
    // ✅ 用 markRaw 标记,防止 Vue 代理 ECharts 实例
    this.chart = markRaw(instance)
  }
}

如果使用 Composition API:

js 复制代码
import { ref, markRaw } from 'vue'

// ✅ 方式一:shallowRef(只代理顶层,不深度代理值本身)
const chart = shallowRef(null)
chart.value = echarts.init(el)   // 内部值不会被代理

// ✅ 方式二:markRaw + ref
const chart = ref(null)
chart.value = markRaw(echarts.init(el))

// ❌ 错误:普通 ref,ECharts 实例会被深度代理
const chart = ref(null)
chart.value = echarts.init(el)
相关推荐
赛博切图仔2 小时前
前端性能内卷终点?Signals 正在重塑我们的开发习惯
前端·javascript·vue.js
Highcharts.js3 小时前
抉择之巅:从2029年回望2026年——企业可视化“战略分水岭”?
前端·javascript·信息可视化·编辑器·echarts·highcharts
米丘3 小时前
Vite 开发服务器启动时,如何将 client 注入 HTML?
javascript·node.js·vite
一 乐3 小时前
饮食营养信息|基于springboot + vue饮食营养管理信息平台系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·论文·毕设·饮食营养管理信息系统
军军君013 小时前
数字孪生监控大屏实战模板:空气污染监控
前端·javascript·vue.js·typescript·前端框架·echarts·数字孪生
米丘3 小时前
vite 插件 @vitejs/plugin-vue
javascript·node.js·vite
冰暮流星3 小时前
javascript之DOM更新操作
开发语言·javascript·ecmascript
练习前端两年半3 小时前
Vue3 KeepAlive 深度揭秘:组件缓存的魔法是如何实现的?
前端·vue.js·面试
吃西瓜的年年3 小时前
react(四)
前端·javascript·react.js