深入分析 Vue 3 源码中 `reactive` 对象的 `isShallow` 和 `isReadonly` 标志位 (`__v_isShallow`, `__v_isReadonly`) 的实现,以及它们如何控制 `Proxy` 的行为。

Vue 3 源码探秘:reactive 对象的 isShallowisReadonly 之谜

哈喽大家好!今天咱们来聊聊 Vue 3 响应式系统里两个挺重要的标志位:__v_isShallow__v_isReadonly。 别看它们名字有点长,其实作用可大了,直接影响着你的数据能不能被深度追踪,能不能被修改。 我们就从 reactive 函数开始,一路追踪到 Proxy 的处理,彻底搞清楚这两个标志位是怎么控制 Proxy 行为的。

1. reactive 函数:响应式世界的入口

首先,让我们回顾一下 reactive 函数,它是创建响应式对象的关键。

// packages/reactivity/src/reactive.ts

import {
  mutableHandlers,
  readonlyHandlers,
  shallowReactiveHandlers,
  shallowReadonlyHandlers
} from './baseHandlers'

import { isObject, toRaw } from '@vue/shared'
import {
  mutableCollectionHandlers,
  readonlyCollectionHandlers,
  shallowCollectionHandlers,
  shallowReadonlyCollectionHandlers
} from './collectionHandlers'

export const reactiveMap = new WeakMap<object, any>()
export const readonlyMap = new WeakMap<object, any>()

export function reactive<T extends object>(target: T): T {
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  )
}

export function readonly<T extends object>(target: T): T {
  return createReactiveObject(
    target,
    true,
    readonlyHandlers,
    readonlyCollectionHandlers,
    readonlyMap
  )
}

export function shallowReactive<T extends object>(target: T): T {
  return createReactiveObject(
    target,
    false,
    shallowReactiveHandlers,
    shallowCollectionHandlers,
    reactiveMap
  )
}

export function shallowReadonly<T extends object>(target: T): T {
  return createReactiveObject(
    target,
    true,
    shallowReadonlyHandlers,
    shallowCollectionHandlers,
    readonlyMap
  )
}

function createReactiveObject(
  target: object,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<object, any>
) {
  if (!isObject(target)) {
    return target
  }
  // target is already a Reactive object, return existing proxy.
  // also avoid circular recursion.
  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }
  // target is already a Readonly object, return existing proxy.
  if (
    proxyMap.has(target)
  ) {
    return proxyMap.get(target)
  }

  // only a plain object can be made reactive.
  if (!canObserve(target)) {
    return target
  }

  const proxy = new Proxy(
    target,
    isCollectionType(target) ? collectionHandlers : baseHandlers
  )
  proxyMap.set(target, proxy)
  return proxy
}

export const enum ReactiveFlags {
  SKIP = '__v_skip',
  IS_REACTIVE = '__v_isReactive',
  IS_READONLY = '__v_isReadonly',
  RAW = '__v_raw',
  IS_SHALLOW = '__v_isShallow',
  IS_REF = '__v_isRef'
}

export const toReactive = <T extends unknown>(value: T): T =>
  isObject(value) ? reactive(value as object) : value

export const toReadonly = <T extends unknown>(value: T): T =>
  isObject(value) ? readonly(value as object) : value

export function isReactive(value: unknown): boolean {
  if (!isObject(value)) {
    return false
  }
  return !!(value as any)[ReactiveFlags.IS_REACTIVE]
}

export function isReadonly(value: unknown): boolean {
  return !!(value as any)[ReactiveFlags.IS_READONLY]
}

export function isShallow(value: unknown): boolean {
  return !!(value as any)[ReactiveFlags.IS_SHALLOW]
}

export function isProxy(value: unknown): boolean {
  return isReactive(value) || isReadonly(value)
}

export const toRaw = <T>(observed: T): T => {
  const raw = observed && (observed as Target)[ReactiveFlags.RAW]
  return raw ? toRaw(raw) : observed
}

export const markRaw = <T extends object>(value: T): T => {
  def(value, ReactiveFlags.SKIP, true)
  return value
}

const objectToString = Object.prototype.toString
const toTypeString = (value: unknown): string => objectToString.call(value)

function isCollectionType(target: any): boolean {
  return (
    toTypeString(target) === '[object Map]' ||
    toTypeString(target) === '[object Set]' ||
    toTypeString(target) === '[object WeakMap]' ||
    toTypeString(target) === '[object WeakSet]'
  )
}

const canObserve = (value: any): boolean => {
  return (
    !value[ReactiveFlags.SKIP] &&
    !Object.isFrozen(value) &&
    !isNative(value)
  )
}

// implementation
function def(obj: object, key: string | symbol, value: any) {
  Object.defineProperty(obj, key, {
    configurable: true,
    enumerable: false,
    value
  })
}

const isNative = (value: any): boolean => {
  return isObject(value) && typeof value.__v_skip === 'boolean'
}

reactive 函数的核心是调用 createReactiveObject 函数。 我们重点关注 createReactiveObject 函数的参数:

  • target: 需要变成响应式的目标对象。
  • isReadonly: 一个布尔值,决定创建的是只读还是可变的响应式对象。
  • baseHandlers: Proxy 的 handlers 对象,定义了 get, set, deleteProperty 等拦截行为。
  • collectionHandlers: 专门用于处理 Map, Set, WeakMap, WeakSet 等集合类型的 handlers。
  • proxyMap: 使用 WeakMap 来存储已经创建过的 proxy 对象,避免重复代理。

createReactiveObject 内部,会检查目标对象是否已经是一个 reactive 或者 readonly 对象。 如果是,直接返回已有的 proxy,避免重复创建。 如果不是,就创建一个 Proxy 实例,并把 target 和对应的 handlers 传递给它。

2. Proxy Handler:拦截行为的定义者

reactivereadonly 的核心行为都体现在它们使用的 Proxy 的 handler 上。 Vue 3 为了提高性能,将 handler 进行了拆分和复用。 主要分为了以下几类:

  • mutableHandlers: 用于 reactive 创建的可变对象。
  • readonlyHandlers: 用于 readonly 创建的只读对象。
  • shallowReactiveHandlers: 用于 shallowReactive 创建的浅层可变对象。
  • shallowReadonlyHandlers: 用于 shallowReadonly 创建的浅层只读对象。
  • mutableCollectionHandlersreadonlyCollectionHandlersshallowCollectionHandlersshallowReadonlyCollectionHandlers: 用于集合类型的 handler

我们以 mutableHandlers 为例,看看它的定义:

// packages/reactivity/src/baseHandlers.ts

import { track, trigger } from './effect'
import { ReactiveFlags, reactive, readonly, toRaw, isObject } from './reactive'
import { isRef } from './ref'
import { TrackOpTypes, TriggerOpTypes } from './operations'

const get = /*#__PURE__*/ createGetter()
const set = /*#__PURE__*/ createSetter()
const deleteProperty = /*#__PURE__*/ createDeleteProperty()

const readonlyGet = /*#__PURE__*/ createGetter(true)
const shallowReadonlyGet = /*#__PURE__*/ createGetter(true, true)

const shallowReactiveGet = /*#__PURE__*/ createGetter(false, true)

export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has: (target, key) => {
    return key in target
  },
  ownKeys: (target) => {
    return Reflect.ownKeys(target)
  }
}

export const readonlyHandlers: ProxyHandler<object> = {
  get: readonlyGet,
  set(target, key) {
    if (__DEV__) {
      console.warn(
        `Set operation on key "${String(key)}" failed: target is readonly.`,
        target
      )
    }
    return true
  },
  deleteProperty(target, key) {
    if (__DEV__) {
      console.warn(
        `Delete operation on key "${String(key)}" failed: target is readonly.`,
        target
      )
    }
    return true
  },
  has: (target, key) => {
    return key in target
  },
  ownKeys: (target) => {
    return Reflect.ownKeys(target)
  }
}

export const shallowReactiveHandlers: ProxyHandler<object> = {
  get: shallowReactiveGet,
  set,
  deleteProperty,
  has: (target, key) => {
    return key in target
  },
  ownKeys: (target) => {
    return Reflect.ownKeys(target)
  }
}

export const shallowReadonlyHandlers: ProxyHandler<object> = {
  get: shallowReadonlyGet,
  set(target, key) {
    if (__DEV__) {
      console.warn(
        `Set operation on key "${String(key)}" failed: target is readonly.`,
        target
      )
    }
    return true
  },
  deleteProperty(target, key) {
    if (__DEV__) {
      console.warn(
        `Delete operation on key "${String(key)}" failed: target is readonly.`,
        target
      )
    }
    return true
  },
  has: (target, key) => {
    return key in target
  },
  ownKeys: (target) => {
    return Reflect.ownKeys(target)
  }
}

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.IS_SHALLOW) {
      return shallow
    } 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 (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }

    if (shallow) {
      return res
    }

    if (isRef(res)) {
      // ref unwrapping - does not apply for Array + integer key.
      return targetIsArray && isIntegerKey(key) ? res : res.value
    }

    if (isObject(res)) {
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}

const set = /*#__PURE__*/ createSetter()

function createSetter() {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    let oldValue = (target as any)[key]
    if (!isReadonly(target)) {
      if (isRef(oldValue) && !isRef(value)) {
        oldValue.value = value
        return true
      }
      const hadKey = hasOwn(target, key)
      const result = Reflect.set(target, key, value, receiver)
      if (hadKey) {
        if (value !== oldValue) {
          trigger(target, TriggerOpTypes.SET, key, value, oldValue)
        }
      } else {
        trigger(target, TriggerOpTypes.ADD, key, value)
      }
      return result
    } else {
      if (__DEV__) {
        console.warn(
          `Set operation on key "${String(key)}" failed: target is readonly.`,
          target
        )
      }
      return true
    }
  }
}

function createDeleteProperty() {
  return function deleteProperty(target: object, key: string | symbol): boolean {
    if (!isReadonly(target)) {
      const hadKey = hasOwn(target, key)
      const result = Reflect.deleteProperty(target, key)
      if (hadKey) {
        trigger(target, TriggerOpTypes.DELETE, key, undefined, undefined)
      }
      return result
    } else {
      if (__DEV__) {
        console.warn(
          `Delete operation on key "${String(key)}" failed: target is readonly.`,
          target
        )
      }
      return true
    }
  }
}

function hasOwn(val: object, key: string | symbol): key is keyof typeof val {
  return Object.prototype.hasOwnProperty.call(val, key)
}

const arrayInstrumentations: Record<string, Function> = {}

;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach((key) => {
  const method = Array.prototype[key]
  arrayInstrumentations[key] = function (...args: any[]) {
    const arr = toRaw(this)
    for (let i = 0, l = this.length; i < l; i++) {
      track(arr, TrackOpTypes.GET, i + '')
    }
    // we need to check if the argument is also a reactive(nested) object
    const res = method.apply(arr, args)

    if (res === -1 || res === false) {
      // not found
      return method.apply(this, args)
    } else {
      return res
    }
  }
})

;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach((key) => {
  const method = Array.prototype[key]
  arrayInstrumentations[key] = function (...args: any[]) {
    // we need to disable tracking inside array methods to avoid duplicate
    // triggers. See issue #q186
    pauseTracking()
    const res = method.apply(this, args)
    resetTracking()
    return res
  }
})

function pauseTracking() {
  throw new Error('Function not implemented.')
}

function resetTracking() {
  throw new Error('Function not implemented.')
}

function isIntegerKey(key: any) {
  return typeof key === 'string' &&
    key === String(Number(key)) &&
    Number.isInteger(Number(key))
}

mutableHandlers 定义了可变对象的 get, set, deleteProperty 等行为。 readonlyHandlers 则禁止 setdeleteProperty 操作,并且在开发环境下会发出警告。

3. __v_isShallow__v_isReadonly:标志位的诞生与意义

现在,我们终于要聚焦到 __v_isShallow__v_isReadonly 这两个标志位了。 它们是在 createGetter 函数中被设置的:

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.IS_SHALLOW) {
      return shallow
    } else if (
      key === ReactiveFlags.RAW &&
      receiver === reactiveMap.get(target)
    ) {
      return target
    }

    // ... 省略其他代码
  }
}

可以看到,createGetter 接收两个参数:

  • isReadonly: 决定是否是只读的。
  • shallow: 决定是否是浅层的。

get 函数内部,当访问 ReactiveFlags.IS_REACTIVEReactiveFlags.IS_READONLYReactiveFlags.IS_SHALLOW 属性时,会返回对应的布尔值。 这些布尔值就存储在 __v_isReactive__v_isReadonly__v_isShallow 属性中。

那么,这些标志位有什么用呢?

  • __v_isReadonly: 这个标志位用于标识对象是否是只读的。 如果一个对象被标记为只读,那么任何尝试修改它的属性都会被拦截,并且在开发环境下会发出警告。
  • __v_isShallow: 这个标志位用于标识对象是否是浅层的。 如果一个对象被标记为浅层,那么只有对象的第一层属性会被追踪。 深层嵌套的对象不会被转换成响应式对象。

4. __v_isShallow 如何控制 Proxy 的行为?

__v_isShallow 标志位的关键作用在于控制 reactive 函数是否对嵌套对象进行递归处理。

createGetter 函数中,可以看到这段代码:

if (isObject(res)) {
  return isReadonly ? readonly(res) : reactive(res)
}

这段代码的意思是,如果当前访问的属性值是一个对象,那么会递归调用 reactivereadonly 函数,将其也转换成响应式对象。 但是,如果 shallowtrue,也就是创建的是 shallowReactiveshallowReadonly 对象,那么这段代码就不会执行。

例子:

const obj = {
  a: 1,
  b: {
    c: 2
  }
}

const reactiveObj = reactive(obj);
const shallowReactiveObj = shallowReactive(obj);

console.log(isReactive(reactiveObj.b)); // true,因为 reactive 会递归处理
console.log(isReactive(shallowReactiveObj.b)); // false,因为 shallowReactive 不会递归处理

reactiveObj.b.c = 3; // 触发响应式更新
shallowReactiveObj.b.c = 4; // 不触发响应式更新

在这个例子中,reactiveObj 是一个深度响应式对象,所以修改 reactiveObj.b.c 会触发响应式更新。 而 shallowReactiveObj 是一个浅层响应式对象,所以修改 shallowReactiveObj.b.c 不会触发响应式更新。

5. __v_isReadonly 如何控制 Proxy 的行为?

__v_isReadonly 标志位的作用是禁止对对象的修改。

readonlyHandlersshallowReadonlyHandlers 中,setdeleteProperty 拦截器会阻止任何修改操作,并且在开发环境下会发出警告。

例子:

const obj = {
  a: 1
}

const readonlyObj = readonly(obj);

readonlyObj.a = 2; // 报错:Set operation on key "a" failed: target is readonly.
delete readonlyObj.a; // 报错:Delete operation on key "a" failed: target is readonly.

6. 总结

标志位 作用 如何控制 Proxy 行为
__v_isShallow 标识对象是否是浅层的。 浅层响应式对象只追踪第一层属性,深层嵌套的对象不会被转换成响应式对象。 createGetter 中,如果 shallowtrue,则不会递归调用 reactivereadonly 函数,因此深层嵌套的对象不会被代理。
__v_isReadonly 标识对象是否是只读的。 只读对象不允许修改属性。 readonlyHandlersshallowReadonlyHandlers 中,setdeleteProperty 拦截器会阻止任何修改操作,并且在开发环境下会发出警告。

总而言之,__v_isShallow__v_isReadonly 就像两个开关,控制着 reactive 对象行为。 __v_isShallow 控制了响应式追踪的深度,__v_isReadonly 控制了对象的可变性。 掌握了这两个标志位的含义,可以更好地理解 Vue 3 的响应式系统,也能在实际开发中更加灵活地运用它们。

好了,今天的分享就到这里。 希望大家有所收获!下次有机会再一起深入 Vue 3 的源码!

发表回复

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