人人都会AI编程

26.1 响应式进阶 API

更新时间:2026-07-09

第4章我们深入了响应式系统的底层——Proxy 如何劫持对象、track/trigger 如何实现依赖收集与派发更新。日常开发中用的最多的是 refreactive,它们已经覆盖了 90% 的场景。但当你开始处理大数据量、第三方库集成、或需要更细粒度的内存管理时,就需要下面这些“进阶工具”。它们不是谁都每天都写,但在关键时候能让你少踩很多坑。


shallowRef 与 shallowReactive:浅层响应式

有什么用?
默认的 refreactive 会递归地将一个对象内部的所有属性都变成响应式。这在数据规模不大时很方便,但如果是一个几万条数据的数组,或者一个庞大的配置对象,深层代理的创建过程本身就会消耗时间,而且之后对这些深层数据的任何访问都会触发依赖收集,增加不必要的开销。shallowRefshallowReactive 就是把响应式“只做一层”,深层的数据变动不会被自动追踪。

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 默认会把它们也变成响应式。这有时会带来两个问题:

  1. 增加内存和性能开销,尤其该对象永远不需要变化。
  2. 干扰第三方库的内部逻辑——某些对象本身就带有 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_skiptrue 的标记,响应式转换时看到这个标记就会跳过。

适合 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:从响应式“脱壳”

toRawreactive 的逆向操作。给定一个 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 的 setupuseXXX 函数中,我们会创建一些副作用:watchwatchEffectcomputed、甚至通过 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 响应式系统的边界和能力心里有数了。