在 Vue 3 中,reactive 函数用于将一个普通对象转换为响应式对象。下面代码片段中有一个检查 isReadonly(target) 的步骤,如果目标对象是只读的,则直接返回该对象而不进行进一步的响应式处理。让我们深入探讨一下为什么会有这样的设计。
背景
Vue 3 提供了两种主要的响应性转换:reactive 和 readonly。reactive 创建一个响应式对象,可以在修改时自动触发更新,而 readonly 创建一个只读的响应式对象,不能被修改。
代码分析
function reactive(target) {
if (isReadonly(target)) {
return target;
}
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap
);
}
isReadonly(target)
isReadonly(target) 是一个检查函数,用于判断目标对象是否已经被标记为只读。只读对象通常是通过 readonly 函数创建的。
只读对象的设计目的
只读对象的设计目的是防止对象被修改,同时仍然可以利用 Vue 的响应性系统来跟踪依赖。这种设计在某些场景中非常有用,例如:
- 防止意外修改:在某些情况下,你可能希望确保某些数据不会被意外修改。只读对象可以帮助你实现这一点。
- 数据共享:在应用中,有些数据是共享的,但你希望确保这些数据不会被不同的组件或模块修改。
为什么只读对象不再进行响应式处理?
- 避免重复处理:一个对象如果已经是只读的,那么它已经被处理过并且具有响应性特性。再进行一次响应式处理是没有意义的,而且会浪费性能。
- 保持只读属性:如果一个对象已经被标记为只读,再次进行响应式处理可能会破坏只读属性,导致对象可以被修改。这违背了只读对象的设计初衷。
- 一致性和安全性:通过直接返回只读对象,Vue 可以确保对象的只读属性始终保持不变,提供一致性和安全性。
createReactiveObject 函数
createReactiveObject 是一个内部函数,用于创建响应式对象。它接受以下参数:
target:目标对象。isReadonly:是否是只读对象的标志。baseHandlers:普通对象的处理程序。collectionHandlers:集合类型(如 Map, Set)的处理程序。proxyMap:用于存储已创建的代理对象的映射。
如果目标对象已经是只读的,那么 createReactiveObject 不会被调用,因为只读对象不需要再创建一个新的代理对象。
在 Vue 3 的响应性系统中,如果目标对象已经是只读的,则直接返回该对象而不进行进一步的响应式处理,这样设计的原因主要是为了避免重复处理、保持对象的只读属性,以及确保一致性和安全性。这种设计可以提高性能,同时确保只读对象的属性不会被意外修改。
baseHandlers 如何对普通对象进行响应式处理
// 源码中在这里使用
const mutableHandlers = /* @__PURE__ */ new MutableReactiveHandler();
让我们看看MutableReactiveHandler都有啥内容:
class MutableReactiveHandler extends BaseReactiveHandler {
// BaseReactiveHandler第一个参数为是否只读,第二个参数为是否浅层响应式,
// 所以这里constructor 的作用是初始化是否浅层响应式
constructor(isShallow2 = false) {
super(false, isShallow2);
}
// `target`:目标对象,即要被代理的对象
// `receiver`:代理对象 key, value 键和值
set(target, key, value, receiver) {
// 首先,获取目标对象中当前键的旧值 `oldValue`
let oldValue = target[key];
// 处理非浅层响应式对象
if (!this._isShallow) {
// 检查旧值是否是只读的
const isOldValueReadonly = isReadonly(oldValue);
// 如果新值不是浅层的且不是只读的,将旧值和新值转换为原始值
if (!isShallow(value) && !isReadonly(value)) {
oldValue = toRaw(oldValue);
value = toRaw(value);
}
// 如果目标对象不是数组,且旧值是 `Ref` 对象而新值不是 `Ref` 对象
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
if (isOldValueReadonly) {
// 如果旧值是只读的,返回 `false`,表示设置失败
return false;
} else {
// 否则,将旧值的 `value` 属性设置为新值,并返回 `true`,表示设置成功
oldValue.value = value;
return true;
}
}
}
// 判断属性是否存在
// 如果目标对象是数组且键是整数键,判断该键是否小于数组长度。
// 否则,使用 `hasOwn` 函数判断目标对象是否拥有该键。
const hadKey = isArray(target) && isIntegerKey(key) ?
Number(key) < target.length : hasOwn(target, key);
// 使用 `Reflect.set` 方法设置目标对象的属性值
const result = Reflect.set(target, key, value, receiver);
// 触发依赖更新
- 如果目标对象等于原始接收器对象(即代理对象):
1.如果键不存在,触发 `add` 操作。
2.如果值发生变化,触发 `set` 操作。
if (target === toRaw(receiver)) {
if (!hadKey) {
trigger(target, "add", key, value);
} else if (hasChanged(value, oldValue)) {
trigger(target, "set", key, value, oldValue);
}
}
return result;
}
}