shallowRef、markRaw 和 toRaw 分别适合什么场景?
简化版
shallowRef 只追踪 .value 替换,不深层代理内部对象;markRaw 标记对象不被 Vue 转成响应式;toRaw 从响应式代理取回原始对象。它们都用于控制响应式成本和边界,常见于大对象、第三方实例和性能敏感场景。
详细版
普通 ref 包含对象时,内部对象会被深层响应式处理。
当对象很大、来自第三方库、或不希望被代理时,可以使用浅层或原始对象能力。
const chart = shallowRef(null)
chart.value = createChart(dom)
const rawMap = markRaw(new Map())
const original = toRaw(state)
shallowRef 适合“只关心整体替换”;markRaw 适合“永远不要代理这个对象”;toRaw 适合“临时拿原始对象做比较、传给第三方库或调试”。不要把它们当作绕过 Vue 更新机制的常规手段。
完整版教学
一、Vue 深层响应式有收益,也有成本
Vue 的响应式会代理对象属性,让读取和修改都能被追踪。 这对普通业务状态很方便。 但如果对象非常大,或对象本身由第三方库管理,深层代理就可能没有收益甚至带来问题。
const state = reactive({
user: { name: 'Tom' },
list: [{ id: 1 }, { id: 2 }]
})
假设一个图表实例内部有 500 个字段、几十个方法和循环引用。 把它深层响应式化没有业务价值,因为页面并不依赖这些内部字段渲染。 这时应控制响应式边界。
二、shallowRef 只关心 value 这一层
shallowRef 会追踪 .value 的替换,但不会把内部对象深层转成响应式。
当你只需要“对象换了就更新”,不需要追踪对象内部字段时,它很合适。
const chart = shallowRef(null)
onMounted(() => {
chart.value = createChart(container.value)
})
function resize() {
chart.value?.resize()
}
| 操作 | shallowRef 是否触发依赖 | 原因 |
|---|---|---|
chart.value = newChart | 是 | .value 被替换 |
chart.value.resize() | 否 | 内部方法调用不追踪 |
chart.value.options.title = 'A' | 否 | 深层属性不响应 |
如果需要更新视图,应该替换 .value,或者在必要时手动触发。
不要期待深层字段变更能自动刷新。
三、markRaw 是明确告诉 Vue 不要代理
markRaw(obj) 会给对象打标记,使它不会被转换成响应式代理。
它适合第三方类实例、不可变大对象、复杂库对象等。
例如 ECharts、地图实例、编辑器实例通常不应该被 Vue 深代理。
import { markRaw } from 'vue'
const editor = markRaw(createEditor({
theme: 'dark'
}))
第三方实例
├─ 内部状态由库维护
├─ 方法依赖 this 或私有字段
└─ Vue 深代理可能无收益或有风险
如果一个页面创建 3 个编辑器实例,每个实例内部字段很多,markRaw 能减少不必要的代理和追踪。 它也是在表达所有权:这个对象归第三方库管理,不归 Vue 响应式系统管理。
四、toRaw 是临时拿回原始对象
reactive 返回的是代理对象。
有时你需要拿到原始对象,比如和第三方库做严格引用比较、调试、序列化前处理。
toRaw(proxy) 可以返回被代理前的对象。
const raw = { id: 1 }
const state = reactive(raw)
console.log(state === raw) // false
console.log(toRaw(state) === raw) // true
但 toRaw 不应该长期保存并用来绕过响应式更新。
如果你拿着 raw 对象到处改,Vue 可能追踪不到变化。
它更像临时查看原件,而不是新的状态管理入口。
五、三者的选择看你要控制哪条边界
shallowRef 控制“只追踪外壳替换”。
markRaw 控制“这个对象不要被代理”。
toRaw 控制“从代理临时回到原始对象”。
| API | 控制点 | 典型用途 | 风险 |
|---|---|---|---|
shallowRef | 只追踪 value | 图表实例、组件实例、大对象整体替换 | 深层变更不更新 |
markRaw | 禁止代理对象 | 第三方库实例、不可变对象 | 内部变化不可响应 |
toRaw | 取原始对象 | 比较、调试、传给库 | 长期持有会绕开追踪 |
这三个 API 都不是日常状态的首选。
普通表单、列表、页面状态仍用 ref、reactive 更直观。
六、性能优化要有具体收益
不要看到大对象就条件反射使用浅层响应式。 如果模板确实依赖对象内部字段,浅层处理会让更新变得麻烦。 如果对象内部字段永远不参与渲染,深层响应式才是浪费。
需要深层响应:
form.user.name 改了页面要更新
适合浅层或 raw:
chartInstance.resize() 由库自己处理
记忆钩子:shallowRef 看盒子换没换,markRaw 贴免检标签,toRaw 临时看原件。
面试中可以用“图表实例”作为例子。 这个例子能同时说明第三方对象、不需要深层代理、只关心生命周期和整体引用。
七、常见误区与追问
- 误区:shallowRef 里的对象内部变化也会自动更新视图。 它只追踪
.value层,深层属性变化不会自动触发依赖。 - 误区:markRaw 能提升所有对象性能。 只有不需要响应式代理的对象才适合标记,普通业务状态不应乱用。
- 误区:toRaw 可以长期当状态来改。 长期持有 raw 并修改会绕过 Vue 追踪,容易造成视图不同步。
- 追问:第三方图表实例为什么适合 markRaw? 其内部状态由库维护,Vue 深层代理没有渲染收益,还可能影响方法和性能。
- 追问:shallowRef 和 ref 最大区别是什么? 对对象值,
ref会深层响应式处理,shallowRef只追踪.value替换。 - 追问:什么时候仍应使用普通 reactive? 模板和计算逻辑需要追踪对象内部字段时,应使用普通响应式。
八、加强记忆
这题按“深层代理成本”切入。Vue 默认深层响应式适合业务状态;第三方实例、大对象和只看整体替换时才考虑 shallowRef、markRaw、toRaw。回答时把三者分别定位成看盒子、免代理、取原件,再补不要绕开响应式长期改 raw。