第4章我们深入了响应式系统的底层——Proxy 如何劫持对象、track/trigger 如何实现依赖收集与派发更新。日常开发中用的最多的是 ref 和 reactive,它们已经覆盖了 90% 的场景。但当你开始处理大数据量、第三方库集成、或需要更细粒度的内存管理时,就需要下面这些“进阶工具”。它们不是谁都每天都写,但在关键时候能让你少踩很多坑。
shallowRef 与 shallowReactive:浅层响应式
有什么用?
默认的 ref 和 reactive 会递归地将一个对象内部的所有属性都变成响应式。这在数据规模不大时很方便,但如果是一个几万条数据的数组,或者一个庞大的配置对象,深层代理的创建过程本身就会消耗时间,而且之后对这些深层数据的任何访问都会触发依赖收集,增加不必要的开销。shallowRef 和 shallowReactive 就是把响应式“只做一层”,深层的数据变动不会被自动追踪。
shallowRef
- 只对
.value的访问做响应式处理,不关心.value内部的属性变化。 - 当你修改
.value指向的整个对象时(比如替换成另一个对象),会触发更新。但如果只修改.value.someProp,不会触发。
<script setup>
import { shallowRef, triggerRef } from 'vue'
const state = shallowRef({ count: 0 })
// 这样不会触发更新 ❌
function increment() {
state.value.count++
// 视图不会变化
}
// 这样会触发更新 ✅
function replace() {
state.value = { count: state.value.count + 1 }
}
// 如果实在需要手动通知 Vue “这个 shallowRef 内部变了”,可以用 triggerRef
function forceUpdate() {
state.value.count++
triggerRef(state) // 强制触发依赖更新
}
</script>
shallowReactive
- 只对一个对象的顶层属性做响应式处理,深层嵌套的对象是普通对象(不会转为 Proxy)。
- 对于像
{ a: { b: 1 } }这样的结构,修改state.a(整个替换 a 的值)会触发更新,但修改state.a.b不会。
import { shallowReactive } from 'vue'
const config = shallowReactive({
theme: 'light',
options: { debug: false }
})
// 触发视图更新
config.theme = 'dark'
config.options = { debug: true } // 替换整个 options 对象
// 不触发视图更新
config.options.debug = true // 深层属性绕过响应式
真实使用场景
- 渲染一个巨大的数据列表,而列表项内部的字段不需要响应式(比如一次加载后只整体替换数组或分页切换)。
- 对接第三方插件实例(如地图、编辑器实例),你只关心实例有没有被重新赋值,而不关心它内部的属性变化。
markRaw:标记一个对象“永不代理”
什么场景需要它?
当你把某些对象挂到响应式数据上时,Vue 默认会把它们也变成响应式。这有时会带来两个问题:
- 增加内存和性能开销,尤其该对象永远不需要变化。
- 干扰第三方库的内部逻辑——某些对象本身就带有
getter/setter或私有状态,被 Proxy 包裹后可能行为异常。
markRaw 标记一个对象,告诉 Vue “永远不要把它变成响应式”。之后即便你把这个对象赋值给一个 reactive 的属性,它内部依然是原始对象。
import { reactive, markRaw } from 'vue'
const mapInstance = new Map() // 假设这是一个复杂的地图实例
const state = reactive({
widgets: []
})
// 加入一个永远不会变的普通对象
state.widgets.push({
id: 1,
tool: markRaw(mapInstance) // tool 不会被转为响应式
})
真相:markRaw 本质上是在对象上设置了一个 __v_skip 为 true 的标记,响应式转换时看到这个标记就会跳过。
适合 markRaw 的典型候选者:
- 第三方类实例(ECharts 实例、富文本编辑器实例等)。
- 不可变数据(Immutable.js 对象、Object.freeze 返回的对象也可以直接配合)。
- 无需在模板中绑定、也不参与计算属性的静态数据。
readonly 与 shallowReadonly:只读保护
有时你需要把一个响应式数据暴露给其他组件或模块,但禁止它们直接修改——比如一个全局配置、或父组件通过 provide 传递的数据。readonly 可以为响应式对象创建一个只读的“视图”,任何试图修改它的操作都会在开发环境下报出警告。
import { reactive, readonly } from 'vue'
const original = reactive({ count: 0 })
const copy = readonly(original)
// 这样会报警告 ⚠️
copy.count++
// original 依然可以正常修改,并且 copy 会同步变化
original.count++ // copy.count 也跟着变
同样有浅层版本 shallowReadonly,只对顶层属性做只读限制,内部属性仍然可以修改(同样在开发环境会告警)。
实践中的用法
- 将
reactive的状态通过readonly返回给外部,保护内部状态不被意外篡改。 - 在 Pinia Store 中,如果你希望某个 state 只允许通过 actions 修改,可以在组件侧用
readonly包裹后再暴露。
toRaw:从响应式“脱壳”
toRaw 是 reactive 的逆向操作。给定一个 reactive 生成的 Proxy 对象,toRaw 返回它背后的原始对象。
import { reactive, toRaw } from 'vue'
const obj = { count: 0 }
const state = reactive(obj)
console.log(toRaw(state) === obj) // true
什么时候需要它?
- 提交表单数据给后端时,没必要发送一份 Proxy 副本,
toRaw拿回原始数据。 - 集成某些只认普通对象的库(例如需要把数据传给一个不接受 Proxy 的图形库)。
注意:toRaw 只对 reactive 生成的对象有效,对 ref 使用会原样返回(因为 ref 的响应式在 .value 上)。
effectScope:集中管理副作用
在组合式 API 的 setup 或 useXXX 函数中,我们会创建一些副作用:watch、watchEffect、computed、甚至通过 onMounted 注册的生命周期。这些副作用在组件卸载时会自动清理,但在自定义 Hooks 中,如果你在一个不再需要的 Hook 里手动创建了它们,就需要手动停止。
effectScope 创造了一个可管理的“副作用范围”,你可以一次性停止里面所有响应式副作用的依赖收集和后续执行。
import { effectScope, ref, watch } from 'vue'
function createCounter() {
const scope = effectScope()
const count = ref(0)
scope.run(() => {
watch(count, (val) => {
console.log('count changed:', val)
// 如果 count 变化频繁,这里会一直输出
})
})
// 返回增加方法和停止方法
return {
count,
increment: () => count.value++,
stop: () => scope.stop()
}
}
// 使用
const { count, increment, stop } = createCounter()
increment() // 日志: count changed: 1
stop() // 停止作用域内的所有 watch/computed 等
increment() // 没有任何日志输出,watch 已被销毁
实用的思考
- 如果你在写一个自己手动触发的异步 Hook(比如轮询请求),
effectScope可以确保在调用stop后彻底清除定时器和响应式副作用,避免内存泄漏。 - 大部分应用其实不需要直接用到它,因为组件自动处理了作用域。但当你开始写框架级的工具函数时,它会变得很有用。
选型小结
| API | 作用 | 典型场景 |
|-----|------|---------|
| shallowRef | 只有 .value 替换会触发更新 | 大数据数组、第三方实例引用 |
| shallowReactive | 只有顶层属性替换会触发更新 | 大型配置对象、表单数据块 |
| markRaw | 阻止对象被响应式 | 第三方库实例、不可变数据 |
| readonly | 创建只读代理 | 暴露给外部的受保护状态 |
| toRaw | 提取原始对象 | 传给后端或非 Proxy 兼容库 |
| effectScope | 批量管理副作用生命周期 | 自定义 Hooks 的清理逻辑 |
这些进阶 API 并不是要你每天都用,但它们在你“觉得响应式好像有些碍事”的时候,提供了精准的控制力。理解了它们,你就对整个 Vue 响应式系统的边界和能力心里有数了。