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
refreceives an object, its.valuewill be wrapped byreactive(), becoming deeply reactive. - If it receives a primitive value,
convertreturns the primitive value directly, but the primitive is now stored in the_valueof theRefImplinstance.
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:
refwraps a primitive value into a{ value: rawValue }object, proxied by aRefImplinstance, intercepting.valueaccess via getter/setter to achieve reactivity. - Unboxing: In templates and
reactiveobjects, Vue automatically calls.valueto get the actual value of aref, providing syntactic convenience. - Linking: The
refs created bytoRef/toRefsmaintain 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.