在 Vue 3 的响应式系统中,readonly 是一个用于创建只读代理对象的关键 API。它通过 Proxy 拦截所有可能修改对象的操作,并阻止这些操作的执行,从而确保对象的状态不可变。更进一步,Vue 还会在开发环境下对非法修改尝试抛出警告,帮助开发者及时发现潜在错误。本节将深入剖析 readonly 的实现原理,揭示其如何拦截各类修改操作并提供友好的开发时反馈。
一、readonly 的核心目标
readonly 的主要用途包括:
- 防止意外修改:保护全局状态、配置对象或从父组件传递的 props 不被子组件意外修改。
- 性能优化:只读对象不需要建立响应式依赖,可以跳过
track操作,减少运行时开销。 - 语义清晰:明确标识一个对象是不可变的,提升代码可读性和可维护性。
与 reactive 不同,readonly 创建的对象是“深只读”的——不仅根对象不可变,其嵌套的所有对象和数组也同样被转换为只读。
二、实现原理:Proxy 拦截器的全面封锁
readonly 的核心实现依赖于 Proxy,通过定义一个特殊的 handler 来拦截所有可能修改对象的操作。以下是 readonly handler 的关键部分:
const readonlyHandler = {
get: createGetter(true),
set(target, key) {
// 开发环境警告
if (__DEV__) {
console.warn(
`Set operation on key "${String(key)}" failed: target is readonly.`,
target
);
}
return true; // 在严格模式下会抛出 TypeError
},
deleteProperty(target, key) {
if (__DEV__) {
console.warn(
`Delete operation on key "${String(key)}" failed: target is readonly.`,
target
);
}
return true;
},
has: createGetter(true),
ownKeys: createGetter(true)
};核心拦截器解析:
get拦截器:- 与
reactive类似,get拦截属性读取。 - 关键区别:在
createGetter(isReadonly = true)中,不会调用track进行依赖收集。因为只读对象的状态永远不会改变,没有必要建立响应式依赖。 - 同样进行深层处理:如果获取的值是对象,递归调用
readonly将其也转换为只读。
- 与
set拦截器:- 这是阻止修改的核心。
- 函数体为空,不执行
Reflect.set,直接阻止赋值操作。 - 在开发环境(
__DEV__)下,打印警告信息,提示开发者试图修改只读对象。 - 返回
true表示操作成功。这看似矛盾,实则是为了兼容非严格模式。在严格模式下(如模块脚本),返回true但未实际设置属性会抛出TypeError;在非严格模式下,操作静默失败。
deleteProperty拦截器:- 阻止
delete obj.key操作。 - 同样在开发环境下发出警告。
- 返回
true,行为与set相同。
- 阻止
has和ownKeys:- 使用
createGetter(true),意味着允许读取操作(如'key' in obj或Object.keys(obj)),但不会进行依赖追踪。
- 使用
三、深层只读的实现机制
readonly 如何确保嵌套对象也是只读的?答案在于 get 拦截器中的递归处理:
function createGetter(isReadonly) {
return function get(target, key, receiver) {
const result = Reflect.get(target, key, receiver);
if (isObject(result)) {
// 如果值是对象,根据 isReadonly 参数决定是否递归
return isReadonly ? readonly(result) : reactive(result);
}
return result;
};
}当 isReadonly 为 true 时,createGetter 会返回一个始终调用 readonly 的函数。这意味着:
const state = readonly({
user: { name: 'Vue', settings: { theme: 'dark' } }
});
// 访问 state.user
// 触发 get → 发现 user 是对象 → 调用 readonly(user) → 返回新的只读代理
// 再访问 state.user.settings → 同样被转换为只读这种惰性递归(lazy recursion)确保了只有在实际访问嵌套对象时,才将其转换为只读,避免了不必要的代理开销。
四、开发时警告的设计哲学
readonly 在开发环境下抛出警告,而不是直接抛出错误,体现了 Vue 对开发体验的精心设计:
- 友好性:警告不会中断应用运行,允许开发者在控制台看到问题后逐步修复。
- 可追溯性:警告信息包含被修改的
key和target对象,便于定位问题源头。 - 生产环境无开销:在生产构建中,
__DEV__被替换为false,警告代码被移除,不影响性能。
// 生产环境构建后
set(target, key) {
return true; // 仅剩一行,极简
}五、与 shallowReadonly 的对比
Vue 还提供了 shallowReadonly,它只将对象的第一层属性设置为只读,而嵌套对象保持原样。
const obj = shallowReadonly({
nested: { count: 0 }
});
obj.nested.count++; // ✅ 允许,nested 是普通对象
obj.name = 'new'; // ❌ 阻止,第一层属性只读shallowReadonly 的实现更简单,get 拦截器不递归调用 readonly:
function shallowGet(target, key, receiver) {
track(target, key); // 即使是 shallow,也需要 track?
return Reflect.get(target, key, receiver);
// 不对结果进行 isObject 判断和转换
}注意:shallowReadonly 仍可能调用 track,因为它需要支持响应式。真正的“浅”体现在不递归代理嵌套对象。
六、实际应用场景
1. 组件 Props 的保护
在 <script setup> 中,defineProps 返回的对象本质上是 shallowReadonly 的:
<script setup>
const props = defineProps(['title']);
// 以下操作在开发环境下会警告
props.title = 'New Title'; // ❌ 警告:Cannot assign to read only property
</script>这防止了子组件意外修改父组件的状态,强制使用事件机制进行通信。
2. 全局常量与配置
import { readonly } from 'vue';
const CONFIG = readonly({
API_URL: 'https://api.example.com',
FEATURES: {
darkMode: true,
analytics: false
}
});
// 任何尝试修改 CONFIG 的操作都会被阻止
CONFIG.API_URL = '...'; // ❌ 静默失败 + 开发警告3. 响应式状态的只读视图
有时你希望暴露状态的一部分作为只读接口:
const store = reactive({
count: 0,
messages: []
});
// 提供只读版本供外部使用
export const readonlyStore = readonly(store);
// 内部仍然可以修改
function increment() {
store.count++;
}七、底层细节:为什么返回 true?
set 和 deleteProperty 拦截器返回 true 的行为可能令人困惑。其背后的 JavaScript 机制如下:
- 在非严格模式下,如果
set拦截器返回true,即使没有实际设置属性,JavaScript 引擎也会认为操作成功,不会报错。 - 在严格模式下(如 ES 模块),如果
set拦截器返回true但属性未被设置,引擎会抛出TypeError。
Vue 利用这一点:
- 在开发环境下,通过
console.warn提供即时反馈。 - 在生产环境下,返回
true确保行为一致,同时移除警告代码以优化体积。
八、总结
readonly 的实现原理可以概括为:
- 全面拦截:通过
Proxy拦截set、deleteProperty等所有修改操作,阻止其执行。 - 深层递归:在
get拦截器中惰性地将嵌套对象也转换为只读。 - 开发警告:在非生产环境下对非法修改尝试发出警告,辅助调试。
- 无依赖追踪:不调用
track,优化只读对象的性能。
readonly 不仅是一个简单的“冻结”工具,更是 Vue 响应式系统中确保数据流清晰、防止副作用污染的重要机制。理解其内部工作方式,有助于开发者更好地利用它来构建健壮、可维护的应用程序。