编程 shallowRef、markRaw 与 Suspense:Vue 3 里哪些 API 只该用在边界上

2026-09-24 21:30:49

shallowRef、markRaw 与 Suspense:Vue 3 里哪些 API 只该用在边界上

Vue 默认的深层响应式适合绝大多数业务状态。只有在大型不可变数据、第三方类实例、外部状态系统或特殊渲染边界中,才需要 shallowRef、markRaw 这类逃生舱 API。下面逐个说清它们的适用位置,另外还有 Teleport、异步组件、KeepAlive,以及仍处于实验状态的 Suspense。

一、shallowRef:只追踪 .value 替换

shallowRef() 不会把内部对象转成深层响应式。只有替换整个 .value 才会触发依赖:

import { shallowRef } from 'vue'

interface Dashboard {
title: string
widgets: Array
}

const dashboard = shallowRef({ title: 'Overview', widgets: [] })

dashboard.value.title = 'Metrics' // 修改内部属性不会触发更新

// 推荐:替换根值
dashboard.value = { ...dashboard.value, title: 'Metrics' }

适合的场景:

  • 把大型不可变对象作为整体替换;
  • 保存图表、编辑器等第三方实例;
  • 与外部状态系统集成;
  • 避免 Vue 深度代理不需要观察的数据。

如果确实要在原对象上修改并手工通知依赖,可以用 triggerRef()

import { shallowRef, triggerRef } from 'vue'

const state = shallowRef({ count: 0 })
state.value.count++
triggerRef(state)

频繁依赖 triggerRef() 通常说明数据更新方式不够清晰,优先考虑不可变替换。

二、shallowReactive:只有根属性响应式

const state = shallowReactive({ count: 0, nested: { enabled: false } })

state.count++ // 会触发更新
state.nested.enabled = true // 嵌套对象本身不是响应式的
state.nested = { enabled: true } // 替换根属性会触发更新

不要把浅层响应式对象嵌进深层响应式树中,这会产生难以解释的混合行为。它更适合作为清晰的状态根边界。

三、readonly 与 shallowReadonly

readonly() 返回一个深层只读代理:

const state = reactive({ count: 0 })
const publicState = readonly(state)

state.count++ // 合法,publicState 会同步反映变化
// publicState.count++ // 开发环境警告

它保护的是通过只读代理发生的修改,不会冻结原对象。常见用途是在 provide() 时提供只读状态,并同时提供修改动作。

shallowReadonly() 只保护根属性,嵌套对象仍可修改。除非边界明确,否则深层 readonly() 更符合「使用者不可写」的直觉。

四、toRaw():临时访问原始对象

toRaw() 返回 Vue Proxy 背后的原始对象,适合临时传给严格检查对象身份、不能接收 Proxy 的外部 API。

不要长期保存 rawForm,也不要通过它修改状态:对原始对象的写入会绕过响应式触发,容易造成界面与数据不一致。序列化普通业务对象时通常不需要先调用 toRaw()JSON.stringify() 可以读取响应式代理的可枚举属性。

五、markRaw():让对象保持非响应式

class MapController {
destroy() {}
}

const state = reactive({ controller: markRaw(new MapController()) })

markRaw() 适合第三方类实例、Vue 组件定义或不应被代理的复杂对象。它只保证被标记的根对象不会转为 Proxy;内部未标记对象如果之后进入响应式系统,仍可能被代理。不要对普通业务数据大面积使用它来「优化性能」。

六、customRef():自定义追踪与触发

customRef() 封装一个防抖 ref:

// composables/useDebouncedRef.ts
import { customRef, onScopeDispose, type Ref } from 'vue'

export function useDebouncedRef(initialValue: T, delay = 300): Ref {
let value = initialValue
let timer: ReturnType | undefined

onScopeDispose(() => {
if (timer) clearTimeout(timer)
})

return customRef((track, trigger) => ({
get() {
track()
return value
},
set(nextValue) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
value = nextValue
trigger()
}, delay)
},
}))
}

customRef() 的 getter 不应每次创建新的对象,否则父子组件比较和更新可能出现意外行为。

七、Teleport:改变 DOM 位置,不改变组件关系

弹窗在组件树中可能嵌得很深,但 DOM 通常希望放在 body 下,以避开祖先的 overflowtransform 和层叠上下文。Teleport 可以移动渲染位置,例如用 `` 包住一个带 role="dialog"aria-modalaria-labelledby 的确认弹窗,并用 @click.self 关闭。

Teleport 只改变真实 DOM 的位置,Props、Emits、注入和逻辑父子关系仍按组件树工作。生产级对话框还要处理焦点圈定、Esc 关闭、关闭后恢复焦点和页面滚动锁定。to 指向的目标在挂载时必须存在,也可以通过 :disabled 暂时关闭传送。

八、异步组件

路由页面通常直接使用动态导入;普通组件可以使用 defineAsyncComponent()

const AnalyticsPanel = defineAsyncComponent({
loader: () => import('./AnalyticsPanel.vue'),
loadingComponent: LoadingPanel,
errorComponent: ErrorPanel,
delay: 200,
timeout: 10_000,
})

不要把首屏必需的小组件全部异步化。代码分割有网络请求和调度成本,应优先用于体积大、低频或条件出现的功能。

九、Suspense 仍是实验性功能

可以等待组件树中的异步 `setup()` 或异步组件,并显示统一 fallback。使用 顶层 await 的组件会成为异步依赖。

三个边界:

  • 截至本文更新时,Vue 官方仍把 Suspense 标为实验性 API;
  • Suspense 本身不提供错误 UI,应结合 onErrorCaptured() 或上层错误处理;
  • Vue Router 的路由懒加载与 Suspense 异步依赖不同,不会仅因为动态导入路由就自动触发 fallback。

常规请求页面自行维护 loading / error / data 往往更直接。只有需要协调一棵异步组件树时再考虑 Suspense。

十、KeepAlive:缓存动态组件实例


被缓存组件会触发 onActivated()onDeactivated()。缓存不是越多越好:实例、DOM 和订阅仍会占用资源,应通过 includeexcludemax 控制范围。

十一、全局 API 属于应用实例

Vue 3 的全局注册和配置都放在 createApp() 返回的应用实例上:app.component / app.directive / app.use / app.config.errorHandler / app.mount。这让同一页面上的多个 Vue 应用可以拥有隔离配置。全局注册会降低依赖可见性,普通业务组件仍优先局部导入。

十二、使用逃生舱 API 的原则

  • 先用默认深层响应式,确认真实性能问题后再引入浅层 API;
  • 在状态根边界使用浅层 API,不要随意混合深层和浅层代理;
  • 第三方实例优先 shallowRefmarkRaw
  • toRaw 只做临时互操作,不作为长期写入口;
  • Suspense 是实验性能力,升级 Vue 时要验证行为;
  • 性能优化应以测量为依据,而不是根据对象「看起来很大」猜测。

小结

shallowRefshallowReactivetoRawmarkRawcustomRef 都是边界工具,不是默认状态 API。Teleport 解决 DOM 挂载位置,异步组件解决代码分割,KeepAlive 解决实例缓存;Suspense 能协调异步依赖,但目前仍需谨慎采用。

官方资料:Advanced Reactivity APITeleportAsync ComponentsSuspenseKeepAlive

复制全文 生成海报 Vue Vue3 响应式 Composition API 前端

推荐文章

程序员茄子在线接单