Source-Code Level Analysis of Vue3's Reactive System: Reactive's Different Strategies for Reference Types vs. Primitive Values and Deep Reactive Conversion Mechanism
1. Problem Description
In Vue3's reactive system, reactive and ref are two core reactive APIs. This topic delves into how the reactive API handles different types of data at the source code level: Why can reactive only be used for object types (Object, Array, Map, Set) and not directly for primitive values (string, number, boolean, etc.)? It also explains the implementation mechanism of its deep reactive conversion.
2. Core Principle Overview
The core of reactive is a proxy mechanism based on ES6 Proxy. Proxy can only proxy object types (reference types) and cannot directly proxy primitive values. Vue3 uses ref to wrap primitive values, making them reactive. reactive recursively converts all nested properties of an object to reactive, a process known as "deep reactive conversion".
3. Step-by-Step Source Code Analysis
Step 1: Entry Point of reactive and Type Checking
In packages/reactivity/src/reactive.ts, the reactive function first checks the type of the incoming value:
export function reactive(target: object) {
// If the target is already a readonly proxy, return it directly
if (isReadonly(target)) {
return target
}
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap
)
}
Key Points:
- Parameter Type Restriction: The type declaration
target: objectalready restricts the input to object types only. - Primitive Value Handling: If a primitive value is passed, TypeScript will report a compilation error, and at runtime, an error will be thrown because Proxy cannot proxy it.
Step 2: Core Function for Creating Reactive Objects
The createReactiveObject function executes the following logic:
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>
) {
// 1. If it's not an object, return directly (warning in development mode)
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// 2. If it's already a proxy and the type matches, return the cached proxy
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// 3. Only specific types can be made reactive
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
// 4. Create the proxy
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
proxyMap.set(target, proxy)
return proxy
}
Step 3: Type Determination and Categorized Handling
The getTargetType function categorizes targets into three types:
enum TargetType {
INVALID = 0, // Invalid type
COMMON = 1, // Ordinary objects/arrays
COLLECTION = 2 // Map, Set, WeakMap, WeakSet
}
function getTargetType(value: Target) {
// Objects marked as non-reactive (e.g., with __v_skip flag)
if (value[ReactiveFlags.SKIP] || !Object.isExtensible(value)) {
return TargetType.INVALID
}
// Check if it's a collection type
if (isMap(value) || isSet(value) || isWeakMap(value) || isWeakSet(value)) {
return TargetType.COLLECTION
}
return TargetType.COMMON
}
Key Points:
- Primitive Values: Filtered out during the
isObjectcheck, whereisObjectreturnsfalse. - Collection Types: Use different handlers
collectionHandlersbecause Map/Set operations (like get, set, add) require special interception.
Step 4: Implementation of Deep Reactive Conversion
In packages/reactivity/src/baseHandlers.ts, the get interceptor handles property reading:
const get = /*#__PURE__*/ createGetter()
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
// ... Handle built-in properties (like __v_isReactive)
const res = Reflect.get(target, key, receiver)
// If it's a built-in Symbol or a non-trackable key, return directly
if (isSymbol(key) && builtInSymbols.has(key) || key === ReactiveFlags.IS_REACTIVE) {
return res
}
// Don't collect dependencies for readonly objects
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}
// For shallow reactive, return the value directly
if (shallow) {
return res
}
// Key to deep reactive conversion: if the value is an object, recursively call reactive
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
Triggering Timing for Deep Conversion:
- Lazy Conversion: Recursive conversion only occurs when a property is accessed, not by converting all nested properties at once when creating the proxy. This improves initial performance.
- Conditional Check:
isObject(res)checks if the value is an object type; if so, it callsreactive(res)orreadonly(res). - Caching Mechanism: The
reactivefunction has an internalproxyMapcache, preventing duplicate proxying of the same object.
Step 5: Handling Strategy for Primitive Values
For primitive values, Vue3 provides the ref API:
export function ref(value?: unknown) {
return createRef(value, false)
}
function createRef(rawValue: unknown, shallow: boolean) {
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
class RefImpl<T> {
private _value: T
private _rawValue: T
constructor(value: T, public readonly __v_isShallow: boolean) {
// If the value is an object, wrap it with reactive
this._value = __v_isShallow ? value : toReactive(value)
}
get value() {
track(this, TrackOpTypes.GET, 'value')
return this._value
}
set value(newVal) {
// ... Trigger updates
}
}
export const toReactive = <T extends unknown>(value: T): T =>
isObject(value) ? reactive(value) : value
Key Design Points:
- Wrapper Object:
refwraps primitive values in a{ value: ... }object, making them proxy-able. - Automatic Unwrapping: When accessing a
refin templates or within areactiveobject,.valueis automatically unwrapped. - Object Handling: If a
ref's value is an object,reactiveis called internally for deep conversion.
4. Summary of Design Principles
- Limitations of Proxy: Proxy can only proxy objects, which is a language-level limitation, not a design flaw in Vue.
- Type Safety: Errors are caught early through TypeScript type declarations and runtime checks.
- Performance Optimization:
- Lazy conversion: Nested properties are converted on-demand.
- Caching mechanism: Avoids duplicate proxying.
- Special handling for collection types: Optimizes performance for Map/Set.
- Separation of API Responsibilities:
reactive: Handles object types, providing deep reactivity.ref: Handles primitive values, also supports objects (internally uses reactive).shallowReactive: Provides shallow reactivity without recursive conversion.
5. Key Points for Interview Answers
When asked "Why can't reactive handle primitive values?", you can answer as follows:
- Root Cause: The mechanism of ES6 Proxy restricts it to only proxy object types.
- Solution: Use
refto wrap primitive values and access them via.value. - Deep Conversion Principle:
reactiverecursively calls itself within the getter, achieving lazy deep reactivity. - Performance Considerations: On-demand conversion + caching mechanism avoids unnecessary performance overhead.
- Type Design: Provides compile-time checks through TypeScript's type system, improving code robustness.
This design adheres to JavaScript's language features while offering a complete and efficient reactive solution through reasonable API分工.