直接用 readonly 包裹 reactive 对象可创建响应式但不可修改的副本,拦截写操作、允许读取,嵌套结构默认深度只读;需确保嵌套对象本身为响应式,配合状态管理时应在返回前封装 readonly,避免对 props 重复包装或错误使用 toRefs。

直接用 readonly 包裹 reactive 创建的对象,就能得到一个响应式但不可修改的副本。它不冻结原始数据,而是拦截所有写操作——读取照常、赋值被拦,开发时警告、生产时静默失败。
基础组合:先 reactive,再 readonly
这是最常用也最安全的方式:
- 用
reactive({})创建可变的响应式对象 - 立即用
readonly()包一层,返回只读代理 - 原始对象仍可自由修改,只读代理会自动同步更新(共享同一响应式系统)
示例:
const state = reactive({ count: 0, user: { name: 'Alice' } })<br>const safeState = readonly(state)<br><br>console.log(safeState.count) // ✅ 正常读取<br>safeState.count = 1 // ⚠️ 开发环境报错,生产环境无反应<br>state.count = 5 // ✅ 原对象改了,safeState.count 自动变成 5嵌套结构自动受保护(深度只读)
readonly 默认是深度只读,无需额外配置:
-
safeState.user.name = 'Bob'会被拦截 -
safeState.items[0].id = 999同样失败 - 前提是嵌套对象本身也是响应式的(比如用
reactive({})创建的)
⚠️ 注意:如果某层是普通对象(如 API 直接返回的 plain object),它不会被自动转为响应式,readonly 也无法拦截对它的修改。补救方式是手动包装该层,或改用 shallowReadonly + 显式控制。
适用于用户提出以下需求时:“构建赢回活动(win-back campaign)”、“重新激活已流失订阅者(re-engage lapsed subscribers)”、“执行重新授权 / 重新同意(re-permission / re-consent)清理”或“清理无效联系人列表(sunset my dead list)”;输出一个闭环式再激活方案——包括:流失用户群组(lapsed-cohort)定义、分阶段优惠阶梯(staged offer ladder)、重新授权(re-consent / re-p
配合状态管理模块使用(类 Pinia 场景)
在自定义 store 或组合式函数中暴露只读状态时,必须在返回前封装:
- 不要返回原始
reactive对象,而要返回readonly(state) - 方法(如 actions)仍可操作原始状态,但外部无法绕过逻辑直接改数据
- 类型上也能配合 TypeScript 推导出只读属性,编辑器标红误赋值
示例:
const state = reactive({ count: 0, message: 'Hello' })<br>export function useMyStore() {<br> return {<br> state: readonly(state), // ✅ 外部只能读<br> increment: () => state.count++,<br> setMessage: (msg: string) => state.message = msg<br> }<br>}避免常见错误
这些做法看似合理,实则无效或有害:
- 在子组件里对
props再调一次readonly—— props 本身已是只读代理,多包一层无意义,还可能干扰类型 - 先
toRefs再readonly—— 解构后失去只读性;正确顺序是toRefs(readonly(obj)) - 把非响应式对象塞进 reactive 的嵌套层 —— 只读保护会漏掉那部分,需提前用
reactive或readonly手动包裹










