Skip to content

响应式进阶

本节对标 Vue 3 源码 @vue/reactivity 中的 readonly.tsbaseHandlers.ts

本节目标

实现 reactive 模块中的进阶 API:

  • readonly — 只读响应式代理
  • shallowReactive — 浅层响应式
  • shallowReadonly — 浅层只读
  • shallowRef — 浅层 ref
  • toRaw / markRaw — 获取原始对象 / 标记为非响应式
  • 工具判断函数

Handler 抽象

Vue 3 源码中,reactive / readonly / shallowReactive / shallowReadonly 共用一套 handler 工厂函数,通过参数控制行为差异:

ts
// createGetter 是 getter 工厂函数,根据 isReadonly 和 shallow 参数生成不同行为的 get 拦截器
function createGetter(isReadonly = false, shallow = false) {
  return function get(target: object, key: string | symbol, receiver: object) {
    // 拦截特殊标记 key 的访问,用于 isReactive() / isReadonly() 等工具函数的判断
    if (key === ReactiveFlags.IS_REACTIVE) {
      // 如果不是 readonly,则说明是 reactive 的
      return !isReadonly
    }
    if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    }
    if (key === ReactiveFlags.RAW) {
      // 访问 __v_raw 时返回原始对象(toRaw 的实现基础)
      return target
    }

    // 通过 Reflect.get 获取属性值,保证 this 指向正确
    const res = Reflect.get(target, key, receiver)

    if (!isReadonly) {
      // 只有非 readonly 才需要收集依赖,因为 readonly 数据不会变化
      track(target, key)
    }

    if (shallow) {
      // shallow 模式:不对嵌套对象做递归代理,直接返回原始值
      return res
    }

    if (isRef(res)) {
      // 自动解包 ref:在 reactive 对象中访问 ref 属性时自动返回 .value
      // 数组的 length 属性除外,避免特殊情况
      return key !== 'length' ? res.value : res
    }

    if (isObject(res)) {
      // 深层代理:嵌套对象惰性地转为 reactive 或 readonly(访问时才转换,而非一次性递归)
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}

// createSetter 是 setter 工厂函数,shallow 参数决定是否对新值做 toRaw 处理
function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object,
  ) {
    // 获取旧值,用于后续比较
    const oldValue = (target as any)[key]
    if (!shallow) {
      // 非 shallow 模式下,将新值转为原始值再存储,避免代理对象嵌套
      value = toRaw(value)
    }
    // 使用 Reflect.set 设置属性值
    const result = Reflect.set(target, key, value, receiver)
    if (hasChanged(value, oldValue)) {
      // 只有值真正发生变化时才触发更新,避免不必要的重新渲染
      trigger(target, key)
    }
    return result
  }
}

四种组合

APIisReadonlyshallowget 行为set 行为
reactivefalsefalsetrack + 深层递归 reactivetrigger
readonlytruefalse不 track + 深层递归 readonly警告,不允许 set
shallowReactivefalsetruetrack + 不递归trigger
shallowReadonlytruetrue不 track + 不递归警告,不允许 set

实现 readonly

ts
// readonly 创建一个只读的深层响应式代理
export function readonly<T extends object>(target: T): Readonly<T> {
  // 第二个参数 true 表示 isReadonly,使用 readonlyHandlers 作为 Proxy handler
  return createReactiveObject(target, true, readonlyHandlers)
}

// readonly 专用的 Proxy handlers
const readonlyHandlers: ProxyHandler<object> = {
  // 使用 createGetter(true, false):不收集依赖,深层递归为 readonly
  get: createGetter(true, false),
  set(target, key) {
    // set 操作不允许执行,仅输出警告帮助开发者定位问题
    console.warn(`Set operation on key "${String(key)}" failed: target is readonly.`, target)
    return true // 返回 true 避免在严格模式下抛出 TypeError
  },
  deleteProperty(target, key) {
    // delete 操作同样不允许,输出警告
    console.warn(`Delete operation on key "${String(key)}" failed: target is readonly.`, target)
    return true
  },
}

readonly 的核心特点:

  • 不收集依赖 — 因为数据永远不会变,收集了也不会触发
  • set 时警告 — 开发者友好的错误提示
  • 深层递归 — 嵌套对象也是 readonly

实现 shallowReactive / shallowReadonly

ts
// shallowReactive:只对第一层属性做响应式代理,嵌套对象不会递归转换
export function shallowReactive<T extends object>(target: T): T {
  // isReadonly=false, 使用 shallowReactiveHandlers(getter 中 shallow=true)
  return createReactiveObject(target, false, shallowReactiveHandlers)
}

// shallowReadonly:只对第一层属性做只读代理,嵌套对象不会递归转换
export function shallowReadonly<T extends object>(target: T): Readonly<T> {
  // isReadonly=true, 使用 shallowReadonlyHandlers(getter 中 isReadonly=true, shallow=true)
  return createReactiveObject(target, true, shallowReadonlyHandlers)
}

shallow 版本只对第一层属性进行代理,嵌套对象保持原样:

ts
const state = shallowReactive({ nested: { count: 0 } })
isReactive(state)          // true
isReactive(state.nested)   // false — 嵌套对象不是响应式的

实现 shallowRef

ts
// shallowRef:创建浅层 ref,内部值为对象时不会自动转换为 reactive
export function shallowRef<T>(value: T): Ref<T> {
  return new ShallowRefImpl(value)
}

// ShallowRefImpl 与 RefImpl 类似,但不对内部值做深层响应式转换
class ShallowRefImpl<T> {
  private _value: T // 存储当前值(原始值,不做 reactive 转换)
  public dep: Dep = new Set() // 依赖集合,存储所有订阅了该 ref 的 effect
  public readonly __v_isRef = true // ref 标识符,用于 isRef() 判断

  constructor(value: T) {
    this._value = value  // 直接赋值,不调用 convert/toReactive,这是与 ref 的核心区别
  }

  get value() {
    // 读取 .value 时收集依赖
    trackRefValue(this)
    return this._value
  }

  set value(newVal: T) {
    if (hasChanged(newVal, this._value)) {
      this._value = newVal  // 直接赋新值,不做 reactive 转换
      triggerRefValue(this) // 触发所有依赖更新
    }
  }
}

ref 的区别:对象类型不会自动转换为 reactive

使用场景:当内部值是一个大对象(如组件实例),不需要深层响应式时,用 shallowRef 避免性能开销。

toRaw 与 markRaw

toRaw — 获取原始对象

ts
// toRaw:获取代理对象对应的原始对象,剥离 Proxy 包装
export function toRaw<T>(observed: T): T {
  // 通过访问 ReactiveFlags.RAW(__v_raw)获取原始对象,这会被 getter 拦截并返回 target
  const raw = observed && (observed as any)[ReactiveFlags.RAW]
  // 递归调用 toRaw,处理多层 proxy 嵌套的情况(如 readonly(reactive(obj)))
  return raw ? toRaw(raw) : observed
}

递归解包,处理多层 proxy 嵌套的情况。

markRaw — 标记为永不响应式

ts
// markRaw:标记一个对象为"永远不应该被转为响应式代理"
export function markRaw<T extends object>(value: T): T {
  // 在对象上定义 __v_skip 标记,reactive() 会检查该标记并跳过代理
  Object.defineProperty(value, ReactiveFlags.SKIP, {
    configurable: true, // 允许后续删除或重新定义
    enumerable: false, // 不可枚举,不影响 for...in 等遍历
    value: true, // 标记值为 true
  })
  return value
}

reactive 中检查该标记:

ts
// reactive 函数中对 markRaw 标记的检查
export function reactive<T extends object>(target: T): T {
  if ((target as any)[ReactiveFlags.SKIP]) {
    // 如果对象被 markRaw 标记过(__v_skip 为 true),直接返回原对象,不创建 Proxy
    return target
  }
  // ...
}

使用场景:第三方库的复杂对象(如 Leaflet Map 实例)不应该被 reactive 化。

工具判断函数

ts
// ReactiveFlags 枚举定义了特殊的内部标记 key
export const enum ReactiveFlags {
  SKIP = '__v_skip', // markRaw 使用的跳过标记
  IS_REACTIVE = '__v_isReactive', // 用于判断是否是 reactive 代理
  IS_READONLY = '__v_isReadonly', // 用于判断是否是 readonly 代理
  RAW = '__v_raw', // 用于获取代理对应的原始对象
}

// isReactive:通过访问 __v_isReactive 判断对象是否为 reactive 代理
// 访问该 key 会被 Proxy 的 get 拦截器捕获,返回 !isReadonly 的结果
export function isReactive(value: unknown): boolean {
  return !!(value && (value as any)[ReactiveFlags.IS_REACTIVE])
}

// isReadonly:通过访问 __v_isReadonly 判断对象是否为 readonly 代理
export function isReadonly(value: unknown): boolean {
  return !!(value && (value as any)[ReactiveFlags.IS_READONLY])
}

// isProxy:判断对象是否为任何类型的响应式代理(reactive 或 readonly)
export function isProxy(value: unknown): boolean {
  return isReactive(value) || isReadonly(value)
}

这些函数利用 Proxy 的 get 拦截实现 —— 访问特殊标记 key 时返回对应布尔值。

统一的工厂函数

ts
// createReactiveObject:统一的代理对象创建工厂,reactive/readonly/shallow 系列都通过此函数创建
function createReactiveObject(
  target: object, // 原始对象
  isReadonly: boolean, // 是否为只读代理
  baseHandlers: ProxyHandler<object>, // Proxy 的 handler(决定 get/set 的具体行为)
) {
  // 根据是否 readonly 选择不同的缓存 Map(同一对象可以同时有 reactive 和 readonly 两种代理)
  const proxyMap = isReadonly ? readonlyMap : reactiveMap

  // 检查缓存:如果该对象已经被代理过,直接返回已有代理,避免重复创建
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }

  // 创建新的 Proxy 代理对象
  const proxy = new Proxy(target, baseHandlers)
  // 将代理存入缓存 Map,key 为原始对象,value 为 Proxy
  proxyMap.set(target, proxy)
  return proxy
}

用两个 WeakMap 分别缓存 reactive 和 readonly 的代理,同一对象可以同时有两种代理。

测试用例

ts
// readonly 的测试套件
describe('readonly', () => {
  // 测试:readonly 对象的值可以读取,且本身和嵌套对象都是 readonly
  it('should make values readonly', () => {
    const original = { foo: 1, bar: { baz: 2 } } // 原始对象含嵌套结构
    const wrapped = readonly(original) // 创建 readonly 代理
    expect(wrapped.foo).toBe(1) // 可以正常读取属性值
    expect(isReadonly(wrapped)).toBe(true) // 代理对象本身是 readonly
    expect(isReadonly(wrapped.bar)).toBe(true) // 嵌套对象也被深层递归为 readonly
  })

  // 测试:对 readonly 对象赋值时应该触发警告
  it('should warn on set', () => {
    console.warn = vi.fn() // mock console.warn 以便检测是否被调用
    const wrapped = readonly({ foo: 1 })
    wrapped.foo = 2 // 尝试修改 readonly 对象
    expect(console.warn).toHaveBeenCalled() // 断言 warn 被调用(说明修改被拦截并警告)
  })
})

// shallowReactive 的测试套件
describe('shallowReactive', () => {
  // 测试:shallowReactive 不会将嵌套对象转为响应式
  it('should not make nested objects reactive', () => {
    const state = shallowReactive({ nested: { count: 0 } })
    expect(isReactive(state)).toBe(true) // 第一层是响应式的
    expect(isReactive(state.nested)).toBe(false) // 嵌套对象不是响应式的(shallow 的核心行为)
  })
})

// shallowRef 的测试套件
describe('shallowRef', () => {
  // 测试:shallowRef 的内部对象不会被转为 reactive
  it('should not convert nested objects', () => {
    const a = shallowRef({ count: 1 }) // 创建 shallowRef,值为对象
    expect(isReactive(a.value)).toBe(false) // 断言内部对象不是 reactive(与 ref 的行为不同)
  })
})

本节小结

  1. Handler 工厂模式 — 通过 isReadonlyshallow 参数组合出 4 种行为
  2. readonly — 不收集依赖、set 警告、深层递归
  3. shallow 系列 — 只代理第一层,性能更优
  4. toRaw / markRaw — 逃逸机制,处理不需要响应式的场景
  5. 工具函数 — 利用 Proxy get 拦截实现类型判断

下一节搭建响应式模块的测试框架。

用心学习,用代码说话 💻