Vue 3 `markRaw` 详解:什么时候该让一个对象"别被代理"

一句话总结:markRaw 的作用是把一个对象标记为"原始对象",让它永远、在任何情况下都不会被 Vue 的响应式系统转成 Proxy。它常用于包裹第三方库实例、不可变的静态大对象,避免无意义的响应式开销或代理导致的运行时错误。


一、什么是 markRaw

markRaw 是 Vue 3 响应式 API 中的一个工具函数。它的行为非常纯粹:

import { markRaw } from 'vue'

const obj = markRaw({ foo: 1 })

执行后,obj 上会被挂一个不可枚举、不可配置、不可写的内部标记 __v_skip = true。从这一刻起,无论你把它传给 reactive()ref()shallowReactive() 还是放进别的响应式对象里,Vue 都不会为它创建响应式代理,而是原样返回这个对象本身

import { reactive, markRaw } from 'vue'

const raw = markRaw({ foo: 1 })
const state = reactive({ nested: raw })

console.log(state.nested === raw) // true,没有被代理
state.nested.foo = 2             // 直接改原始对象,不会触发响应式更新

二、为什么需要它

Vue 3 的响应式是基于 ES6 Proxy深层代理:当你 reactive(obj) 时,Vue 会惰性地把 obj 的每一层嵌套对象都包成 Proxy,以便深度追踪变化。

这套机制对"普通数据"很完美,但遇到下面三种情况就会出问题或浪费性能:

场景

问题

第三方库实例(ECharts、Monaco、TinyMCE、Leaflet…)

这些实例内部有自己的状态机和 DOM 引用,被 Proxy 包裹后可能出现诡异的报错、死循环或性能塌方

不可变的静态配置 / 大对象(如一份几千行的字典表)

你从来不需要追踪它的变化,深层代理却会为它每一层都创建 Proxy,纯属浪费内存和初始化时间

某些带循环引用的对象

代理时可能触发栈溢出或无限递归

markRaw 就是为解决这些问题而生的"响应式豁免"开关。


三、基本用法

import { markRaw, reactive } from 'vue'

// 1. 标记第三方实例,避免被代理
const chart = markRaw(new SomeChartLib(container))

// 2. 标记静态数据
const COUNTRY_DICT = markRaw({
  CN: '中国',
  US: '美国',
  // ...几千条
})

// 3. 放进响应式状态里也不会被代理
const state = reactive({
  chart,                 // 保持原始实例
  countryDict: COUNTRY_DICT,
  count: 0,              // 普通字段仍然响应式
})

四、实战场景

场景 1:在组件里持有第三方实例(最常见)

<script setup>
import { markRaw, onMounted, onBeforeUnmount, ref } from 'vue'
import * as echarts from 'echarts'

const el = ref(null)
// 关键:用 markRaw 持有实例,防止被响应式代理
let chart = null

onMounted(() => {
  chart = markRaw(echarts.init(el.value))
  chart.setOption({ /* ... */ })
})

onBeforeUnmount(() => {
  chart?.dispose()
})
</script>

<template>
  <div ref="el" style="width: 600px; height: 400px" />
</template>

经验法则:任何 new 出来的、由别人维护状态的实例,放进响应式系统前都先 markRaw

场景 2:Pinia / 全局状态里存第三方对象

// stores/editor.js
import { defineStore } from 'pinia'
import { markRaw } from 'vue'
import ClassicEditor from '@ckeditor/ckeditor5-build-classic'

export const useEditorStore = defineStore('editor', {
  state: () => ({
    // 必须 markRaw,否则 CKEditor 实例被 Proxy 包裹会出问题
    editor: markRaw(ClassicEditor.create(document.querySelector('#editor'))),
  }),
})

场景 3:超大只读配置表

// 一份几 MB 的静态映射表,绝不参与响应式追踪
export const BIG_STATIC_MAP = markRaw(loadHugeConfig())

五、原理解析(源码视角)

markRaw 的源码非常短:

// vue/src/reactivity/markRaw.ts
export function markRaw<T>(value: T): T {
  if (Object.isObject(value) && !isMarked(value)) {
    def(value, '__v_skip', true)
  }
  return value
}

它通过 Object.defineProperty 给对象挂一个 ReactiveFlags.SKIP(即 __v_skip)标记。

而在 reactive() 创建代理的入口处,第一行就是检查这个标记:

// vue/src/reactivity/reactive.ts
function createReactiveObject(target, ...) {
  // 如果被标记 SKIP,直接返回原对象,不创建代理
  if (target && target.__v_skip /* ReactiveFlags.SKIP */) {
    return target
  }
  // ...正常创建 Proxy
}

因为响应式是按对象惰性代理的,一旦某个对象带了 __v_skip,无论它出现在哪个响应式对象的哪一层属性上,访问它时都会跳过代理、直接返回原始对象 —— 所以 markRaw 的效果在"被包裹"语义上是深层生效的(其内部子对象也不会被代理),但子对象如果被你单独 reactive() 依然可以代理。


六、注意事项与常见坑

1. 标记是永久的、不可撤销

markRaw 没有对应的"取消标记" API。一旦标记,这个对象就永远是 raw 的。如果你之后又想让它响应式,只能重新创建一个新对象

const raw = markRaw({ foo: 1 })
// 想要响应式?重新来过,不能"撤销" markRaw
const reactiveCopy = reactive({ ...raw })

这点和 readonly 不同 —— 但和直觉一致:标记是写死在对象上的。

2. 是"对象级"标记,不是"字段级"

markRaw 标记的是整个对象,不是某个字段。你不能只让 state.chart 不代理、而让 state.count 代理 —— 正确的做法是:把 chart 实例本身 markRaw,再放进 state

3. markRawshallowReactiveshallowRef

这三者经常被混淆,区别如下:

API

行为

典型用途

markRaw(obj)

对象完全不代理(深层),且任何 reactive 包裹都无效

第三方实例、静态大对象

shallowReactive(obj)

只代理顶层属性,嵌套对象不代理

性能优化,自己手动管理嵌套

shallowRef(obj)

只追踪 ref.value整体替换,不深代理内部

持有大对象、自己控制更新时机

核心差异:markRaw永久豁免(连顶层都不代理),shallow* 系列是浅层代理(顶层还是响应式的)。

4. 与 toRaw 的区别

  • toRaw(proxy):从一个已经是响应式代理的对象上,拿回它背后的原始对象。

  • markRaw(raw):把一个原始对象标记成"永远不要被代理"。

一个是从代理回到原始,一个是提前给原始对象盖个"免代理"章,方向相反、用途不同。

5. 在 <script setup>ref

ref(obj) 内部也会 reactive 化对象。markRaw 同样能挡住它:

import { ref, markRaw } from 'vue'

const editor = ref(markRaw(someInstance)) // .value 保持原始实例

七、速查总结

  • ✅ 第三方库实例(图表、编辑器、地图) → 必须 markRaw

  • ✅ 不可变的静态大对象 / 配置表 → markRaw 省性能

  • ❌ 需要响应式追踪的业务数据 → 不要 markRaw

  • ⚠️ 标记后无法撤销,想响应式只能重建对象

  • ⚠️ markRaw 是对象级永久豁免,不是字段级、也不是浅代理

口诀:"外部实例、静态大件,markRaw 一盖保平安;业务数据要追踪,千万别给它盖章。"


附:最小可用示例

<script setup>
import { markRaw, reactive, onMounted, ref } from 'vue'
import * as echarts from 'echarts'

const box = ref(null)
const state = reactive({
  // 普通数据照常响应式
  title: '访问趋势',
  // 第三方实例用 markRaw 保命
  chart: null,
})

onMounted(() => {
  state.chart = markRaw(echarts.init(box.value))
  state.chart.setOption({
    xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed'] },
    yAxis: { type: 'value' },
    series: [{ type: 'line', data: [120, 200, 150] }],
  })
})
</script>

<template>
  <h2>{{ state.title }}</h2>
  <div ref="box" style="width: 100%; height: 360px" />
</template>