Vue3 Reactive System Source Code Level Primitive Type Auto-boxing and Unboxing (Implementation Principle of ref's .value Access Mechanism)

Vue3 Reactive System Source Code Level Primitive Type Auto-boxing and Unboxing (Implementation Principle of ref's .value Access Mechanism)


1. Problem Description

In Vue3's reactive system, ref can handle not only objects but also primitive values (such as numbers, strings, booleans). Primitive values in JavaScript are immutable and cannot be intercepted by Proxy like objects. How does Vue3 achieve reactivity for primitive values? Why do reactive primitive values created via ref need to be accessed via .value? How is the internal automatic "boxing" and "unboxing" mechanism implemented?


2. Core Concept: The Reactivity Dilemma of Primitive Values

  • Primitive Values: number, string, boolean, null, undefined, symbol, bigint.
  • Problem: Proxy can only proxy objects, not primitive values directly. If a primitive value is passed directly to reactive(), it is returned unchanged, making change tracking impossible.

3. Solution: Object Wrapping (Boxing)

Vue3's solution is: Wrap the primitive value into a plain object with a value property, then make this object reactive. This process is called "boxing".

// Pseudo code illustration
function ref(rawValue) {
  // Boxing: Wrap the primitive value into { value: rawValue }
  const refObject = {
    value: rawValue
  }
  // Make this object reactive
  return reactive(refObject)
}

This way, reads and writes to .value can be intercepted by Proxy, enabling dependency tracking and update triggering.


4. Source Code Level Implementation Analysis

Let's dive into the implementation of ref in packages/reactivity/src/ref.ts.

Step 1: Create the Ref Object
class RefImpl<T> {
  private _value: T
  public readonly __v_isRef = true  // Identifier for a ref

  constructor(private _rawValue: T, public readonly __v_isShallow?: boolean) {
    // For shallow refs, no deep reactive conversion; otherwise, convert objects to reactive
    this._value = __v_isShallow ? _rawValue : convert(_rawValue)
  }

  get value() {
    // Dependency collection
    track(this, TrackOpTypes.GET, 'value')
    return this._value
  }

  set value(newVal) {
    // Use hasChanged to check if the value actually changed (handles NaN cases, etc.)
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal
      // Convert new value: if it's an object, make it reactive
      this._value = this.__v_isShallow ? newVal : convert(newVal)
      // Trigger updates
      trigger(this, TriggerOpTypes.SET, 'value', newVal)
    }
  }
}
Step 2: The convert Function Handles Object Conversion
const convert = <T extends unknown>(val: T): T =>
  isObject(val) ? reactive(val) : val
  • If ref receives an object, its .value will be wrapped by reactive(), becoming deeply reactive.
  • If it receives a primitive value, convert returns the primitive value directly, but the primitive is now stored in the _value of the RefImpl instance.
Step 3: The ref Factory Function
function ref(value?: unknown) {
  return createRef(value, false)
}

function createRef(rawValue: unknown, shallow: boolean) {
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

5. Automatic Unboxing (Unwrap) Mechanism

In templates and reactive objects, we can access a ref's value without writing .value. How is this achieved?

5.1 Automatic Unboxing in Templates

During the compilation phase, refs in templates are automatically appended with .value:

<template>
  <div>{{ count }}</div>  <!-- Compiled to count.value -->
</template>
5.2 Automatic Unboxing in reactive Objects

When accessing a ref property within a reactive object, it is also automatically unboxed. This is implemented via the Proxy's get interceptor.

In createGetter within packages/reactivity/src/baseHandlers.ts:

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: object, key: string | symbol, receiver: object) {
    // ... Other logic

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

    // Key point: If the retrieved value is a ref, return its .value (automatic unboxing)
    if (isRef(res)) {
      // For arrays or primitive values, unbox directly; for objects, and if not read-only, dependency collection is performed
      return res.value
    }

    // If it's an object and not shallow, convert to reactive
    if (isObject(res)) {
      return reactive(res)
    }

    return res
  }
}

Note: Automatic unboxing only occurs when a ref is nested within a reactive object or during template compilation. When using ref directly in JavaScript, manual .value access is still required.


6. Special APIs: toRef and toRefs

These two utility functions are also related to automatic unboxing, commonly used to preserve reactive links.

6.1 toRef

Converts a property of a reactive object into a ref, maintaining synchronization with the source property:

function toRef(object: any, key: string) {
  return new ObjectRefImpl(object, key)
}

class ObjectRefImpl {
  constructor(private readonly _object, private readonly _key) {}

  get value() {
    return this._object[this._key]  // Triggers the original object's getter
  }

  set value(newVal) {
    this._object[this._key] = newVal  // Triggers the original object's setter
  }
}

The ref created by toRef is "linked"; modifying its .value directly affects the original object.

6.2 toRefs

Converts each property of a reactive object into a ref, often used to destructure without losing reactivity:

function toRefs(object) {
  const ret: any = {}
  for (const key in object) {
    ret[key] = toRef(object, key)
  }
  return ret
}

7. Summary

  • Boxing: ref wraps a primitive value into a { value: rawValue } object, proxied by a RefImpl instance, intercepting .value access via getter/setter to achieve reactivity.
  • Unboxing: In templates and reactive objects, Vue automatically calls .value to get the actual value of a ref, providing syntactic convenience.
  • Linking: The refs created by toRef/toRefs maintain a link with the source object's property, and modifications are synchronized.

This mechanism allows Vue3 to uniformly handle reactivity for both primitive values and objects while maintaining API simplicity.