先记住这个答案
当组合式函数需要维护更新规则或多个字段的一致性时,可以返回 readonly 包装的状态,并提供明确动作函数。调用者通过只读代理直接写入会受到限制,而内部仍可通过原始响应式状态更新,消费者也能观察这些变化。readonly 主要帮助约束接口使用,不等于复制快照、冻结所有外部引用或建立安全权限。简单局部状态也可以暴露可写 ref,是否只读应由数据所有权和真实更新需求决定。
- 只读代理限制外部直接写,内部合法更新仍可见
- 动作函数承载校验、联动和错误处理规则
- readonly 不是数据副本,也不能替代服务端权限或验证
为什么动作接口有时比任意赋值清楚
分页状态可能要求页码为正整数,修改筛选条件还需要同步重置页码。若调用者可以随意改每个字段,就容易产生不一致组合。把规则集中在动作中,调用方表达要做什么,组合式函数负责维护有效状态。
下面只公开状态视图和 setPage,非法页码被明确拒绝。正常消费者不能通过只读视图直接改 page,但调用 setPage 后会看到更新。只读限制的是访问路径上的写入方式,不是禁止整个系统更新这个对象。
import { reactive, readonly } from 'vue';
function usePage() {
const state = reactive({ page: 1 });
function setPage(value: number) {
if (!Number.isInteger(value) || value < 1) return false;
state.page = value;
return true;
}
return { state: readonly(state), setPage };
}setPage(2) 后只读视图显示 2;setPage(0) 返回 false 且保持 2。尝试直接改返回视图的 page 不应改变内部值,开发环境会提供相应警告。
只读不是快照或深度复制
readonly 包装响应式来源时,读取仍能反映内部后续更新。若需要保存某个时刻的历史值,应另行复制符合业务要求的数据;把只读代理放进历史列表,不会自动保存每个时刻的独立内容。
对普通对象、嵌套引用和特殊对象,也要区分代理约束与真正不可变数据。其他代码若持有来源引用并直接修改,仍可能影响视图;TypeScript 的只读类型也不能在运行时阻止所有外部路径。不要把这种接口设计描述成安全边界或任意对象的深冻结保证。
什么时候无需强制只读
一个仅在组件内部使用的简单输入状态,直接暴露可写 ref 可能更方便,也更符合双向编辑需求。如果每次更新都要经过复杂动作包装却没有实际规则,接口反而增加负担。关键是统一约定,避免一部分字段可随意写、另一部分字段必须通过隐藏规则维护。
采用动作接口后,应说明成功、失败和异步更新状态。例如 setPage 返回 false 时调用方要不要提示,重置动作是否同时清理错误信息,都属于契约。测试不仅要检查不能直接写,还应验证内部合法更新可见,防止把只读接口误实现成永远不更新的副本。
容易答错的地方
- readonly 后内部状态也不能改了
- 内部持有的可变来源仍可更新,只读消费者会观察到这些合法变化。排查“Vue 组合式函数 readonly 返回状态与动作接口”时还要核对输入、版本和执行顺序,并用反例确认修复后的边界。
- 只读对象可直接当历史快照
- 它仍可能连接实时来源,保存历史需要独立的数据快照策略。排查“Vue 组合式函数 readonly 返回状态与动作接口”时还要核对输入、版本和执行顺序,并用反例确认修复后的边界。
面试官还会怎么问?
readonly(ref) 会失去响应能力吗?
不会因只读包装就变成固定值,正常读取仍可反映源 ref 的更新;写入路径受到只读约束。针对“Vue 组合式函数 readonly 返回状态与动作接口”,还应保留最小复现、预期结果和失败路径,避免只凭一次现象下结论。
只提供动作就可以省略输入校验吗?
不能。动作正是集中校验的位置,调用方仍可能传入不符合业务条件的值。针对“Vue 组合式函数 readonly 返回状态与动作接口”,还应保留最小复现、预期结果和失败路径,避免只凭一次现象下结论。
浅只读和深只读怎么选?
取决于希望约束哪一层以及内部对象的共享契约。不要因为外层不能赋值就假定嵌套数据全部受到同样约束。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。