万字长文详解VUE3响应式系统--reactive

336 阅读4分钟

在 Vue 3 中,reactive 函数用于将一个普通对象转换为响应式对象。下面代码片段中有一个检查 isReadonly(target) 的步骤,如果目标对象是只读的,则直接返回该对象而不进行进一步的响应式处理。让我们深入探讨一下为什么会有这样的设计。

背景

Vue 3 提供了两种主要的响应性转换:reactivereadonlyreactive 创建一个响应式对象,可以在修改时自动触发更新,而 readonly 创建一个只读的响应式对象,不能被修改。

代码分析

function reactive(target) {
    if (isReadonly(target)) {
      return target;
    }
    return createReactiveObject(
      target,
      false,
      mutableHandlers,
      mutableCollectionHandlers,
      reactiveMap
    );
}

isReadonly(target)

isReadonly(target) 是一个检查函数,用于判断目标对象是否已经被标记为只读。只读对象通常是通过 readonly 函数创建的。

只读对象的设计目的

只读对象的设计目的是防止对象被修改,同时仍然可以利用 Vue 的响应性系统来跟踪依赖。这种设计在某些场景中非常有用,例如:

  1. 防止意外修改:在某些情况下,你可能希望确保某些数据不会被意外修改。只读对象可以帮助你实现这一点。
  2. 数据共享:在应用中,有些数据是共享的,但你希望确保这些数据不会被不同的组件或模块修改。

为什么只读对象不再进行响应式处理?

  1. 避免重复处理:一个对象如果已经是只读的,那么它已经被处理过并且具有响应性特性。再进行一次响应式处理是没有意义的,而且会浪费性能。
  2. 保持只读属性:如果一个对象已经被标记为只读,再次进行响应式处理可能会破坏只读属性,导致对象可以被修改。这违背了只读对象的设计初衷。
  3. 一致性和安全性:通过直接返回只读对象,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;
    }
  }