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

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


一、什么是 markRaw

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

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

const obj = markRaw({ foo: 1 })

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

js 复制代码
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 就是为解决这些问题而生的"响应式豁免"开关。


三、基本用法

js 复制代码
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:在组件里持有第三方实例(最常见)

vue 复制代码
<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 / 全局状态里存第三方对象

js 复制代码
// 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:超大只读配置表

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

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

markRaw 的源码非常短:

js 复制代码
// 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() 创建代理的入口处,第一行就是检查这个标记:

js 复制代码
// 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 的。如果你之后又想让它响应式,只能重新创建一个新对象

js 复制代码
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 同样能挡住它:

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

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

七、速查总结

  • ✅ 第三方库实例(图表、编辑器、地图) → 必须 markRaw
  • ✅ 不可变的静态大对象 / 配置表 → markRaw 省性能
  • ❌ 需要响应式追踪的业务数据 → 不要 markRaw
  • ⚠️ 标记后无法撤销,想响应式只能重建对象
  • ⚠️ markRaw 是对象级永久豁免,不是字段级、也不是浅代理

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


附:最小可用示例

vue 复制代码
<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>
相关推荐
用户83134859306986 小时前
Vue+Threejs实现人物奔跑场景
vue.js·webgl·three.js
meilindehuzi_a6 小时前
Vue 项目中的前端 BFF:跨域代理与流式请求实战
前端·javascript·vue.js
小林ixn19 小时前
从BFF到SSE:我在Vue项目里藏了个“AI翻译官”
前端·vue.js·vite
Listen·Rain1 天前
Vue3中组件间通信详解
前端·javascript·vue.js
Listen·Rain1 天前
Vue3中customRef详解
前端·javascript·vue.js
濮水大叔1 天前
Hooks 之后,为什么前端仍需要“包裹”能力?
vue.js·react.js·前端框架
daols881 天前
vxe-table 导出自定义插槽模板内容
前端·javascript·vue.js·vxe-table
山东点狮信息科技有限公司1 天前
企业级开源OA系统推荐
vue.js·spring boot·性能优化·系统架构·开源
gis开发之家1 天前
《Vue3 从入门到大神33篇》Vue3 源码详解(三):响应式核心思想——从 Object.defineProperty 到 Proxy
前端·javascript·vue.js·vue3·vue3源码