Vue 3源码深度解析之:`readonly`和`shallowReactive`:如何创建只读或浅层响应式对象。

各位靓仔靓女们,晚上好!我是你们今晚的Vue 3源码解读导游,外号“代码挖掘机”,今天咱们要一起深入Vue 3的“禁地”,探索readonlyshallowReactive这两个有趣的小家伙。

别担心,咱们不会迷路的,我会用最接地气的方式,带大家一层层揭开它们的神秘面纱。准备好了吗?系好安全带,发车咯!

一、开胃小菜:响应式系统的“味道”

在深入readonlyshallowReactive之前,咱们先简单回顾一下Vue 3响应式系统的“味道”。 Vue 3 的响应式系统,核心就是让数据变化驱动视图更新。 当你修改一个响应式对象的值时,Vue 3 就能自动追踪到这个变化,并通知相关的组件进行更新。

那么,问题来了,Vue 3 是怎么做到这一点的呢? 这就得归功于 Proxy 这个强大的武器了。 Vue 3 通过 Proxy 拦截对对象的访问和修改操作,并在这些操作发生时触发相应的依赖追踪和更新机制。

二、主角登场:readonly闪亮登场

好了,开胃菜吃完了,现在咱们的主角之一——readonly 要闪亮登场了!

readonly,顾名思义,就是“只读”的意思。 它可以将一个对象变成只读的,防止你意外地修改它。

想象一下,你有一个非常重要的数据,比如用户的个人信息,你不希望任何组件随意修改它,这时候 readonly 就能派上大用场了。

1. readonly 的用法:

import { readonly } from 'vue';

const original = { name: '张三', age: 30 };
const readOnlyObj = readonly(original);

console.log(readOnlyObj.name); // 输出:张三

// 尝试修改 readOnlyObj 的属性,会收到警告
// readOnlyObj.name = '李四'; // 报错:Set operation on key "name" failed: target is readonly.

看到了吗? 当你尝试修改 readonly 对象时,Vue 3 会发出警告,阻止你的操作。

2. readonly 的源码剖析:

让我们一起挖掘一下 readonly 的源码,看看它是如何实现只读功能的。

Vue 3 中 readonly 的核心实现, 也是基于 Proxy 。 它创建了一个新的 Proxy 对象,并拦截了 set 操作。 当你尝试设置 Proxy 对象的属性时,set 拦截器会被触发,并阻止你的操作。

// Packages/reactivity/src/readonly.ts

import { mutableToReadonly } from './reactive';
import { isObject, toRaw } from '@vue/shared';
import {
  mutableHandlers,
  readonlyHandlers,
  shallowReactiveHandlers,
  shallowReadonlyHandlers
} from './baseHandlers';
import {
  ReactiveFlags,
  reactive,
  isReadonly,
  isReactive,
  targetTypeMap,
  TargetType
} from './reactive';

export const readonly = <T extends object>(
  target: T
): Readonly<T> => {
  return createReadonlyObject(
    target,
    false,
    readonlyHandlers,
    mutableToReadonly
  )
}

function createReadonlyObject(
  target: any,
  isShallow: boolean,
  baseHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<any, any>
) {
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
  // target is already a ReactiveProxy we can return the same one.
  if (
    target[ReactiveFlags.RAW] &&
    !(isShallow && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }
  // target is already a readonly proxy, return that.
  if (proxyMap.has(target)) {
    return proxyMap.get(target)
  }

  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }

  const proxy = new Proxy(
    target,
    baseHandlers
  )
  proxyMap.set(target, proxy)
  return proxy
}

export const enum TargetType {
  INVALID = 0,
  COMMON = 1,
  COLLECTION = 2
}

function targetTypeValidator(raw: object, key: string | symbol) {
  return isObject(raw)
    ? String(key) === '__v_isRef' || key === ReactiveFlags.RAW
      ? TargetType.COMMON
      : (Object.hasOwn(raw, key) ? TargetType.COMMON : TargetType.INVALID)
    : TargetType.INVALID
}
// Packages/reactivity/src/baseHandlers.ts

import { isObject, toRaw, hasChanged } from '@vue/shared'
import { track, trigger } from './effect'
import { ReactiveFlags, reactive, toReactive, toReadonly } from './reactive'
import { isRef, Ref } from '../../packages/reactivity/src/ref'

const get = createGetter()
const set = createSetter()
const readonlyGet = createGetter(true)
const shallowReadonlyGet = createGetter(true, true)

function createGetter(isReadonly: boolean = false, shallow: boolean = false) {
  return function get(target: object, key: string | symbol, receiver: object) {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (key === ReactiveFlags.RAW &&
      receiver === reactiveMap.get(target)
    ) {
      return target
    }

    const targetIsArray = Array.isArray(target)
    if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
      return Reflect.get(arrayInstrumentations, key, receiver)
    }

    const res = Reflect.get(target, key, receiver)

    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
      return res
    }

    if (!isReadonly) {
      track(target, 'get', key)
    }

    if (shallow) {
      return res
    }

    if (isRef(res)) {
      // ref unwrapping - only for reactive deps
      return targetIsArray && isIntegerKey(key)
        ? res
        : res.value
    }

    if (isObject(res)) {
      // Convert returned value into reactive/readonly object.
      return isReadonly ? toReadonly(res) : toReactive(res)
    }

    return res
  }
}

const set = createSetter()

function createSetter() {
  return function set(
    target: object,
    key: string | symbol,
    value: any,
    receiver: object
  ): boolean {
    let oldValue = (target as any)[key]
    if (!hasChanged(oldValue, value)) {
      return true
    }

    if (isReadonly(target)) {
      if (__DEV__) {
        console.warn(
          `Set operation on key "${String(key)}" failed: target is readonly.`,
          target
        )
      }
      return true
    }

    const result = Reflect.set(target, key, value, receiver)
    if (target === toRaw(receiver)) {
      const oldValue = (target as any)[key]
      if (hasChanged(value, oldValue)) {
        trigger(target, 'set', key, value, oldValue)
      }
    }
    return result
  }
}

export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}

export const readonlyHandlers: ProxyHandler<object> = {
  get: readonlyGet,
  set(target: object, key: string | symbol, value: any, receiver: object): boolean {
    if (__DEV__) {
      console.warn(
        `Set operation on key "${String(key)}" failed: target is readonly.`,
        target
      )
    }
    return true
  },
  deleteProperty(target: object, key: string | symbol): boolean {
    if (__DEV__) {
      console.warn(
        `Delete operation on key "${String(key)}" failed: target is readonly.`,
        target
      )
    }
    return true
  }
}

readonlyHandlers 对象中, set 方法直接返回 true,并且在开发环境下还会发出警告。 这样就阻止了对 readonly 对象的修改。

3. 深度 readonly

readonly 默认是深度的,也就是说,如果你的对象中包含嵌套的对象,那么所有嵌套的对象都会被变成只读的。

import { readonly } from 'vue';

const original = {
  name: '张三',
  address: {
    city: '北京',
    street: '长安街'
  }
};

const readOnlyObj = readonly(original);

// 尝试修改嵌套对象的属性,会收到警告
// readOnlyObj.address.city = '上海'; // 报错:Set operation on key "city" failed: target is readonly.

三、另一位主角:shallowReactive 登场

接下来,让我们欢迎另一位主角——shallowReactive

shallowReactive, 顾名思义,就是“浅层响应式”的意思。 它只会将对象的第一层属性变成响应式的,而不会递归地处理嵌套对象。

1. shallowReactive 的用法:

import { shallowReactive } from 'vue';

const original = {
  name: '张三',
  address: {
    city: '北京',
    street: '长安街'
  }
};

const shallowReactiveObj = shallowReactive(original);

// 修改 shallowReactiveObj 的 name 属性,会触发更新
shallowReactiveObj.name = '李四'; // 触发更新

// 修改 shallowReactiveObj.address.city 属性,不会触发更新
shallowReactiveObj.address.city = '上海'; // 不触发更新

看到了吗? 当你修改 shallowReactive 对象的 name 属性时,Vue 3 会触发更新。 但是,当你修改嵌套对象 addresscity 属性时,Vue 3 并不会触发更新。

2. shallowReactive 的源码剖析:

shallowReactive 的源码实现,也基于 Proxy 。 它创建了一个新的 Proxy 对象,并拦截了 getset 操作。 但是,与 reactive 不同的是, shallowReactiveget 拦截器中不会递归地将嵌套对象变成响应式的。

// Packages/reactivity/src/reactive.ts

import {
  mutableHandlers,
  readonlyHandlers,
  shallowReactiveHandlers,
  shallowReadonlyHandlers
} from './baseHandlers';
import {
  isObject,
  toRaw,
  hasOwn,
  isSymbol,
  isArray,
  isMap,
  isSet
} from '@vue/shared';

export const reactive = <T extends object>(target: T): T => {
  if (isReadonly(target)) {
    return target
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableToProxy
  )
}

export const shallowReactive = <T extends object>(target: T): T => {
  return createReactiveObject(
    target,
    true,
    shallowReactiveHandlers,
    shallowToProxy
  )
}

function createReactiveObject(
  target: any,
  isShallow: boolean,
  baseHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<any, any>
) {
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
  // target is already a ReactiveProxy we can return the same one.
  if (
    target[ReactiveFlags.RAW] &&
    !(isShallow && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }
  // target is already a proxy, return that.
  if (proxyMap.has(target)) {
    return proxyMap.get(target)
  }

  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }

  const proxy = new Proxy(
    target,
    baseHandlers
  )
  proxyMap.set(target, proxy)
  return proxy
}

// Packages/reactivity/src/baseHandlers.ts

// 省略很多代码

function createGetter(isReadonly: boolean = false, shallow: boolean = false) {
  return function get(target: object, key: string | symbol, receiver: object) {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (key === ReactiveFlags.RAW &&
      receiver === reactiveMap.get(target)
    ) {
      return target
    }

    const targetIsArray = Array.isArray(target)
    if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
      return Reflect.get(arrayInstrumentations, key, receiver)
    }

    const res = Reflect.get(target, key, receiver)

    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
      return res
    }

    if (!isReadonly) {
      track(target, 'get', key)
    }

    if (shallow) {
      return res // 关键所在:shallow 为 true 时,直接返回 res,不进行深层递归
    }

    if (isRef(res)) {
      // ref unwrapping - only for reactive deps
      return targetIsArray && isIntegerKey(key)
        ? res
        : res.value
    }

    if (isObject(res)) {
      // Convert returned value into reactive/readonly object.
      return isReadonly ? toReadonly(res) : toReactive(res)
    }

    return res
  }
}

createGetter 函数中,当 shallowtrue 时, get 拦截器会直接返回属性值 res,而不会调用 toReactivetoReadonly 将其转换成响应式对象。 这就是 shallowReactive 实现浅层响应式的关键。

3. shallowReactive 的适用场景:

shallowReactive 在某些特定的场景下非常有用。 比如,当你的对象中包含大量的数据,而你只需要监听第一层属性的变化时,使用 shallowReactive 可以提高性能。

另一个常见的场景是,当你的对象中包含一些不受 Vue 3 控制的外部对象时,比如第三方库的对象,使用 shallowReactive 可以避免 Vue 3 尝试劫持这些对象,从而避免一些潜在的问题。

四、shallowReadonly:只读且浅层

既然有 readonlyshallowReactive,那有没有 shallowReadonly 呢? 答案是肯定的!

shallowReadonly 结合了 readonlyshallowReactive 的特点,它会创建一个只读的、浅层响应式对象。 也就是说,你不能修改 shallowReadonly 对象的任何属性,但是它只会阻止你修改第一层属性,而不会递归地阻止你修改嵌套对象的属性。

1. shallowReadonly 的用法:

import { shallowReadonly } from 'vue';

const original = {
  name: '张三',
  address: {
    city: '北京',
    street: '长安街'
  }
};

const shallowReadonlyObj = shallowReadonly(original);

// 尝试修改 shallowReadonlyObj 的 name 属性,会收到警告
// shallowReadonlyObj.name = '李四'; // 报错:Set operation on key "name" failed: target is readonly.

// 尝试修改 shallowReadonlyObj.address.city 属性,不会收到警告,但也不会触发更新
shallowReadonlyObj.address.city = '上海'; // 不报错,不触发更新

2. shallowReadonly 的源码剖析:

shallowReadonly 的源码实现与 readonlyshallowReactive 类似, 也是基于 Proxy 。它结合了 readonlyHandlers 和浅层 reactive 的 get 拦截器。

// Packages/reactivity/src/reactive.ts

export const shallowReadonly = <T extends object>(
  target: T
): Readonly<T> => {
  return createReactiveObject(
    target,
    true,
    shallowReadonlyHandlers,
    shallowReadonlyToProxy
  )
}

// Packages/reactivity/src/baseHandlers.ts

export const shallowReadonlyHandlers: ProxyHandler<object> = {
  get: shallowReadonlyGet,
  set(target: object, key: string | symbol, value: any, receiver: object): boolean {
    if (__DEV__) {
      console.warn(
        `Set operation on key "${String(key)}" failed: target is readonly.`,
        target
      )
    }
    return true
  },
  deleteProperty(target: object, key: string | symbol): boolean {
    if (__DEV__) {
      console.warn(
        `Delete operation on key "${String(key)}" failed: target is readonly.`,
        target
      )
    }
    return true
  }
}

五、总结与对比:

为了帮助大家更好地理解 readonlyshallowReactiveshallowReadonly 的区别,我特意准备了一张表格:

特性 reactive readonly shallowReactive shallowReadonly
响应式 深层 深层 浅层 浅层
只读
修改第一层属性 触发更新 报错 触发更新 报错
修改嵌套属性 触发更新 报错 不触发更新 不报错,不触发更新

六、实战演练:

说了这么多理论,不如来点实际的。 让我们通过一个简单的例子,来演示一下 readonlyshallowReactive 的用法。

假设我们有一个组件,需要显示用户的个人信息。 但是,我们不希望该组件能够修改用户的个人信息。 这时候,我们就可以使用 readonly 来保护用户的数据。

<template>
  <div>
    <p>姓名:{{ userInfo.name }}</p>
    <p>年龄:{{ userInfo.age }}</p>
    <button @click="updateName">修改姓名(无效)</button>
  </div>
</template>

<script>
import { ref, readonly, onMounted } from 'vue';

export default {
  setup() {
    const userInfo = ref({ name: '张三', age: 30 });
    const readOnlyUserInfo = readonly(userInfo);

    const updateName = () => {
      // 尝试修改 readOnlyUserInfo 的 name 属性,会收到警告
      readOnlyUserInfo.value.name = '李四'; // 报错:Set operation on key "name" failed: target is readonly.
    };

    onMounted(() => {
      //即使直接修改userInfo.value,因为已经通过readonly包装,仍然无法修改
      userInfo.value.name = '王五'
    })

    return {
      userInfo: readOnlyUserInfo,
      updateName
    };
  }
};
</script>

在这个例子中,我们使用 readonlyuserInfo 对象变成只读的。 这样,即使我们在组件中尝试修改 userInfoname 属性,也会收到警告,从而保证了用户数据的安全性。

再来一个 shallowReactive 的例子:

假设我们有一个组件,需要显示一个复杂的对象,该对象包含很多嵌套的属性。 但是,我们只需要监听该对象的第一层属性的变化。 这时候,我们就可以使用 shallowReactive 来提高性能。

<template>
  <div>
    <p>姓名:{{ person.name }}</p>
    <p>城市:{{ person.address.city }}</p>
    <button @click="updateName">修改姓名</button>
    <button @click="updateCity">修改城市(无效)</button>
  </div>
</template>

<script>
import { ref, shallowReactive } from 'vue';

export default {
  setup() {
    const person = shallowReactive({
      name: '张三',
      address: {
        city: '北京',
        street: '长安街'
      }
    });

    const updateName = () => {
      person.name = '李四'; // 触发更新
    };

    const updateCity = () => {
      person.address.city = '上海'; // 不触发更新
    };

    return {
      person,
      updateName,
      updateCity
    };
  }
};
</script>

在这个例子中,我们使用 shallowReactiveperson 对象变成浅层响应式的。 这样,当我们修改 personname 属性时,Vue 3 会触发更新。 但是,当我们修改 person.address.city 属性时,Vue 3 并不会触发更新。

七、总结:

好了,今天的源码解读就到这里了。 相信通过今天的学习,大家对 readonlyshallowReactive 已经有了更深入的了解。 记住,它们都是 Vue 3 响应式系统中非常有用的工具,可以帮助我们更好地管理数据,提高性能。

希望今天的讲座对大家有所帮助,咱们下次再见! 拜拜!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注