一句话总结:
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. markRaw ≠ shallowReactive ≠ shallowRef
这三者经常被混淆,区别如下:
| 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>