为什么 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)