封装属性只读设计的核心是控制访问路径而非锁死数据,须由提供方在源头用readonly或shallowReadonly主动封装,配合TS类型与ESLint构建三层防护。

封装属性时做只读设计,核心不是“锁死数据”,而是控制访问路径——让调用方只能读、无法改,同时不破坏响应式更新能力。关键在源头封装、层级明确、配合类型与工具形成防护。
明确谁提供、谁封装
只读必须由数据提供方主动封装,不能等接收方处理。常见错误是子组件对 props 再套一层 readonly,这既多余又可能掩盖原始对象未受保护的问题。
- 父组件用 reactive 创建对象 → 提前用 readonly(obj) 包装后再传给子组件
- 父组件用 ref 创建状态 → 用 readonly(myRef) 得到只读 ref
- Pinia store 中暴露 state → 返回 readonly($state),强制变更走 action
- provide/inject 共享状态 → provide('config', readonly(config)),避免子组件拿到原始响应式对象
选对只读深度:readonly 还是 shallowReadonly
两者都保持响应式,区别只在嵌套层级是否受控:
- readonly:深层只读。适合结构稳定、不允许任何改动的场景,比如用户档案、全局配置、权限信息
- shallowReadonly:仅顶层属性只读。适合根对象需固定(如不能替换整个 stats),但内部字段可更新(如 stats.count++)的场景
例如:shallowReadonly({ list: reactive([]) }) 允许往 list 里 push,但禁止 state.list = newList。
配合类型与静态检查加固
只读代理在运行时拦截修改,但类型系统能在编码阶段提前预警:
- TS 中为 props 接口字段加 readonly 修饰符,如 interface User { readonly id: number; readonly name: string; }
- 启用 ESLint 规则 vue/no-mutating-props,防止子组件直接赋值
- 组合式 API 中使用 defineProps 的泛型声明,自动推导只读性
注意边界情况
readonly 不是万能锁:
- 对基本类型 ref(如 ref(1)),readonly(ref(1)) 只拦截 .value = xxx,不阻止参与运算
- 非响应式普通对象(如 API 直接返回的 plain object)经 readonly 封装后仍不可响应,视图不会更新
- 生产环境静默失败,开发环境才报警告 —— 切勿依赖控制台提示来验证逻辑










