Unlocking Vue 3 Reactivity: Deep Dive into Proxy, Ref, and Computed Internals

This article provides a comprehensive walkthrough of Vue 3's reactivity system, covering the architecture diagram, prerequisite concepts like Proxy, Reflect, and WeakMap, and detailed source code explanations for reactive, readonly, ref, effect, and computed implementations, plus further reading links.

WeDoctor Frontend Technology
WeDoctor Frontend Technology
WeDoctor Frontend Technology
Unlocking Vue 3 Reactivity: Deep Dive into Proxy, Ref, and Computed Internals

Reactivity Architecture Diagram

Reactivity architecture diagram
Reactivity architecture diagram

Prerequisite Knowledge

Proxy (https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy)

Reflect (https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect)

WeakMap (https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/WeakMap)

Source Code Walkthrough

reactive

packages/reactivity/src/reactive.ts

// 扩展被代理对象的标志属性声明
export interface Target {
  [ReactiveFlags.SKIP]?: boolean //是否是不可代理对象,被markRaw()过则为true
  [ReactiveFlags.IS_REACTIVE]?: boolean //是否被reactive代理过
  [ReactiveFlags.IS_READONLY]?: boolean //是否被readonly代理过
  [ReactiveFlags.RAW]?: any //被代理的原对象 const p = reactive(obj); p[ReactiveFlags.RAW] === obj 为true
}
function targetTypeMap(rawType: string) {
  switch (rawType) {
    case 'Object':
    case 'Array':
      return TargetType.COMMON // 普通引用类型
    case 'Map':
    case 'Set':
    case 'WeakMap':
    case 'WeakSet':
      return TargetType.COLLECTION // 集合引用类型
    default:
      return TargetType.INVALID // invalid不可被代理的基本数据类型 int boolean string
  }
}

// 运用ts函数重载机制让reactive有2种不同类型的入参、返回
export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
export function reactive(target: object) {
  // if trying to observe a readonly proxy, return the readonly version.
  if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
    return target
  }
  return createReactiveObject(
    target,
    false, // isReadonly
    mutableHandlers,  // 用于Object Array 类型创建Proxy
    mutableCollectionHandlers // 用于Set Map WeakSet WeakMap 类型创建Proxy
  )
}

// 创建响应式代理对象
function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
  // target已经被代理过,并且不是为了将响应式对象变为只读则直接返回
  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }
  // 从缓存(readonlyMap,reactiveMap)中查找,如果已经被代理过则直接返回
  const proxyMap = isReadonly ? readonlyMap : reactiveMap
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  // 只有非基本类型类能被响应式
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) { // 是否是基本类型
    return target
  }
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  proxyMap.set(target, proxy) // 缓存新代理后的对象
  return proxy
}

packages/reactivity/src/baseHandles.ts

// mutableHandlers是Proxy的代理配置,const r = new Proxy(obj,mutableHandlers)
export const mutableHandlers: ProxyHandler<object> = {
  get: createGetter,
  set: createSetter,
  deleteProperty,
  has,
  ownKeys
}
function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, 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 === (isReadonly ? readonlyMap : reactiveMap).get(target)
    ) {
      // 如果key是'__v_raw未被代理标记属性'且target已被响应式代理过,则直接返回该代理的原对象
      // 应用场景 const originObj = toRaw(reactive(obj)); originObj === obj 为 true
      return target
    }
    const targetIsArray = isArray(target)
    if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
      // 代理数组的 'includes', 'indexOf', 'lastIndexOf' 方法并触发依赖收集
      // 代理数组的 'push', 'pop', 'shift', 'unshift', 'splice' 并触发依赖的副作用effect
      return Reflect.get(arrayInstrumentations, key, receiver)
    }
    const res = Reflect.get(target, key, receiver)
    if (
      isSymbol(key)
        ? builtInSymbols.has(key as symbol)
        : key === '__proto__' || key === '__v_isRef'
    ) {
      return res
    }
    if (!isReadonly) {
      // 如果不是只读代理触发依赖收集
      track(target, TrackOpTypes.GET, key)
    }
    // 如果是shallowReactive()直接返回结果,如果target[key]是引用类型则对该值进行响应式收集
    // 这里充分说明了vue3 reactive()的时候只代理了target的属性这一层,只有当访问target的某一个引用类型属性时才向下继续代理一层,而不是像vue2一样在初始化的时候迭代代理所有引用类型
    if (shallow) {
      return res
    }
    if (isRef(res)) {
      const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
      return shouldUnwrap ? res.value : res
    }
    if (isObject(res)) {
      return isReadonly ? readonly(res) : reactive(res)
    }
    return res
  }
}
function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    const oldValue = (target as any)[key]
    if (!shallow) {
      value = toRaw(value)
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        // 如果不是数组,且旧值是ref类型,新值不是ref类型
        oldValue.value = value
        return true
      }
    } else {
      // 如果是shallowReactive()返回的proxy,修改其属性时不会触发响应式副作用effect
    }
    // 如果是对象返回true,如果是数组看是否是合法下标或length indexOf push等自有属性
    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)
    const result = Reflect.set(target, key, value, receiver)
    // don't trigger if target is something up in the prototype chain of original
    if (target === toRaw(receiver)) {
      if (!hadKey) {
        // 触发该属性的副作用effect,且类型为新增属性
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        // 触发该属性的副作用effect,且类型为修改属性
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}
function deleteProperty(target: object, key: string | symbol): boolean {
  const hadKey = hasOwn(target, key)
  const oldValue = (target as any)[key]
  const result = Reflect.deleteProperty(target, key)
  if (result && hadKey) { // 如果属性存在并删除成功,触发依赖该属性的副作用effect
    trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  }
  return result
}
function has(target: object, key: string | symbol): boolean {
  const result = Reflect.has(target, key)
  if (!isSymbol(key) || !builtInSymbols.has(key)) {
    // 如果不是symbol类型则触发对该属性依赖的收集
    track(target, TrackOpTypes.HAS, key)
  }
  return result
}
function ownKeys(target: object): (string | number | symbol)[] {
  // 触发对该属性依赖的收集
  track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)
  return Reflect.ownKeys(target)
}

readonly

export function readonly<T extends object>(target: T): DeepReadonly<UnwrapNestedRefs<T>> {
  return createReactiveObject(
    target,
    true, // isReadonly
    readonlyHandlers, // 用于Object Array 类型创建Proxy
    readonlyCollectionHandlers // 用于Set Map WeakSet WeakMap 类型创建Proxy
  )
}
export const readonlyHandlers: ProxyHandler<object> = {
  get: readonlyGet, // 与reactive 的 createGetter一样,只是第一个参数为true
  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
  }
}

ref

// 运用ts函数重载机制让ref有4种不同类型的入参、返回
export function ref<T extends object>(value: T): ToRef<T>
export function ref<T>(value: T): Ref<UnwrapRef<T>>
export function ref<T = any>(): Ref<T | undefined>
export function ref(value?: unknown) {
  return createRef(value)
}

// ref底层不是通过proxy实现的,而是自定义类RefImpl
function createRef(rawValue: unknown, shallow = false) {
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

// 将原始数据存储在 _value,拦截定义 value属性的 get set方法实现依赖收集和修改更新响应
class RefImpl<T> {
  private _value: T
  public readonly __v_isRef = true
  constructor(private _rawValue: T, public readonly _shallow = false) {
    // 如果是浅响应则无论是引用类型还是基础类型都直接存储原始数据
    this._value = _shallow ? _rawValue : convert(_rawValue) // 注意covert在下面讲解下
  }
  get value() {
    // get触发依赖收集,toRaw(this)是被ref(data)包裹的原始数据data
    track(toRaw(this), TrackOpTypes.GET, 'value')
    return this._value
  }
  set value(newVal) {
    // 如果新旧值没有变化则不处理
    if (hasChanged(toRaw(newVal), this._rawValue)) {
      this._rawValue = newVal
      this._value = this._shallow ? newVal : convert(newVal)
      // trigger 触发依赖此属性的effect重新执行,toRaw(this)是被ref(data)包裹的原始数据data
      trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
    }
  }
}

// 如果被const r = ref(data)包裹的原始数据data是引用类型,则对引用类型进行响应式处理,否则直接返回基本类型。
// 因为如果不这样做的话,r.value的变化会被get set拦截处理,但是r.value.xxx无法被拦截失去了响应
const convert = <T extends unknown>(val: T): T => isObject(val) ? reactive(val) : val

effect

effect的源码非常具有跳跃性,需要多看上面的响应式原理架构图才能理解

watch\computed\render的时候都会创建effect,所以入口来源复杂,入参也复杂

reactive\ref\computed\watch\render\update访问都会级联触发该属性依赖收集track

reactive\ref\computed修改都会级联触发trigger执行该属性的副作用

effectStack 是当前待执行的effect栈

activeEffect 是全局正在触发的effect,每当一个effect触发新的effect入栈的时候activeEffect都会更新为新的,执行完毕后又从effectStack pop出前一个

当调用watch(getter,scheduler,{onTrack,onTrigger})时,可以简单的理解为触发

effect(getter, {
  lazy: true, // 非computed
  onTrack,
  onTrigger,
  scheduler
})
export function effect<T = any>(fn: () => T, options: ReactiveEffectOptions = EMPTY_OBJ): ReactiveEffect<T> {
  if (isEffect(fn)) {
    fn = fn.raw
  }
  const effect = createReactiveEffect(fn, options)
  if (!options.lazy) { // computed属性懒执行,其他副作用执行触发依赖收集
    effect()
  }
  return effect
}
function createReactiveEffect<T = any>(fn: () => T, options: ReactiveEffectOptions): ReactiveEffect<T> {
  const effect = function reactiveEffect(): unknown {
    if (!effect.active) {
      return options.scheduler ? undefined : fn()
    }
    // effectStack 是当前有效的待执行effect栈
    if (!effectStack.includes(effect)) {
      cleanup(effect) // 可能有多个响应式属性都会触发该effect,但是该effect只会执行一次不会重复执行,所以从所有依赖属性的副作用数组中删除该effect
      try {
        enableTracking() // 只有副作用原函数fn()执行期间收集其依赖的响应式属性,执行完毕后不能再收集
        effectStack.push(effect)
        activeEffect = effect // 当前副作用为全局正在执行的副作用
        return fn()
      } finally {
        // 当前副作用依赖收集完成后退栈并不再触发依赖收集
        effectStack.pop()
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  } as ReactiveEffect
  effect.id = uid++
  effect.allowRecurse = !!options.allowRecurse
  effect._isEffect = true
  effect.active = true
  effect.raw = fn // 存储原始副作用函数
  effect.deps = [] // 该副作用依赖的所有响应式属性
  effect.options = options
  return effect
}

function cleanup(effect: ReactiveEffect) {
  const { deps } = effect
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      deps[i].delete(effect)
    }
    deps.length = 0
  }
}
export const enum TrackOpTypes {
  GET = 'get',
  HAS = 'has',
  ITERATE = 'iterate'
}
export const enum TriggerOpTypes {
  SET = 'set',
  ADD = 'add',
  DELETE = 'delete',
  CLEAR = 'clear'
}
// 依赖收集副作用函数
export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (!shouldTrack || activeEffect === undefined) {
    return
  }
  let depsMap = targetMap.get(target) // targetMap存储所有的proxy代理原target
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key) // depsMap存储某个proxy代理原target里的所有属性
  if (!dep) {
    depsMap.set(key, (dep = new Set())) // dep存储某个proxy代理原target里的某个属性的所有副作用effect
  }
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect)
    activeEffect.deps.push(dep)
    if (__DEV__ && activeEffect.options.onTrack) {
      activeEffect.options.onTrack({
        effect: activeEffect,
        target,
        type,
        key
      })
    }
  }
}
// 依赖副作用触发函数
export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    return
  }
  const effects = new Set<ReactiveEffect>() // 存储本次操作导致的需要执行的副作用集合
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        if (effect !== activeEffect || effect.allowRecurse) {
          effects.add(effect)
        }
      })
    }
  }

  if (type === TriggerOpTypes.CLEAR) {
    // 对某个数组或集合执行清空操作时,该数组的所有副作用都要添加到待执行数组中
    depsMap.forEach(add)
  } else if (key === 'length' && isArray(target)) {
    // 当访问数组length属性时只添加其相关的副作用到待执行数组中
    depsMap.forEach((dep, key) => {
      if (key === 'length' || key >= (newValue as number)) {
        add(dep)
      }
    })
  } else {
    // schedule runs for SET | ADD | DELETE
    if (key !== void 0) { // void 0 === undefined
      add(depsMap.get(key))
    }

    // also run for iteration key on ADD | DELETE | Map.SET
    switch (type) {
      case TriggerOpTypes.ADD:
        if (!isArray(target)) {
          add(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            add(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        } else if (isIntegerKey(key)) {
          // new index added to array -> length changes
          add(depsMap.get('length'))
        }
        break
      case TriggerOpTypes.DELETE:
        if (!isArray(target)) {
          add(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            add(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        }
        break
      case TriggerOpTypes.SET:
        if (isMap(target)) {
          add(depsMap.get(ITERATE_KEY))
        }
        break
    }
  }
  // 创建执行副作用的函数
  const run = (effect: ReactiveEffect) => {
    if (__DEV__ && effect.options.onTrigger) {
      effect.options.onTrigger({
        effect,
        target,
        key,
        type,
        newValue,
        oldValue,
        oldTarget
      })
    }
    if (effect.options.scheduler) {
      // scheduler 可以简单理解为watch(key,cb)的cb
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  }
  effects.forEach(run)
}

computed

// 运用ts函数重载机制让ref有3种不同类型的入参、返回
export function computed<T>(getter: ComputedGetter<T>): ComputedRef<T>
export function computed<T>(options: WritableComputedOptions<T>): WritableComputedRef<T>
export function computed<T>(getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>
  if (isFunction(getterOrOptions)) {
    getter = getterOrOptions
    setter = __DEV__
      ? () => {
          console.warn('Write operation failed: computed value is readonly')
        }
      : NOOP
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }
  return new ComputedRefImpl(
    getter,
    setter,
    isFunction(getterOrOptions) || !getterOrOptions.set // isReadonly
  ) as any
}
class ComputedRefImpl<T> {
  private _value!: T // 当前计算属性返回值
  private _dirty = true // 是否有依赖属性变化导致需要重新求值
  public readonly effect: ReactiveEffect<T>
  public readonly __v_isRef = true;
  public readonly [ReactiveFlags.IS_READONLY]: boolean //是否只读
  constructor(
    getter: ComputedGetter<T>,
    private readonly _setter: ComputedSetter<T>,
    isReadonly: boolean
  ) {
    this.effect = effect(getter, {
      lazy: true, // 初始化时不求值,触发get的时候才求值
      scheduler: () => {
        if (!this._dirty) {
          // 依赖属性发生变化,当前计算属性变脏了,在下次get访问时需要重新求值;触发依赖该计算属性的副作用执行
          this._dirty = true
          trigger(toRaw(this), TriggerOpTypes.SET, 'value')
        }
      }
    })
    this[ReactiveFlags.IS_READONLY] = isReadonly
  }
  get value() {
    if (this._dirty) {
      // 第一次访问或依赖属性发生变化才重新求值
      this._value = this.effect()
      this._dirty = false
    }
    track(toRaw(this), TrackOpTypes.GET, 'value')
    return this._value
  }
  set value(newValue: T) {
    this._setter(newValue)
  }
}

Further Reading

Design Patterns in TypeScript (https://juejin.cn/post/6953423646664687652)

Should You Master Framework Source Details? (https://juejin.cn/post/6926336016152428551)

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

ProxyVue3Reactivitycomputed
WeDoctor Frontend Technology
Written by

WeDoctor Frontend Technology

Official WeDoctor Group frontend public account, sharing original tech articles, events, job postings, and occasional daily updates from our tech team.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.