The principles of Reactive and Ref
Reactive vs Ref Principles
Vue.js's reactivity system is one of its core features, allowing developers to declaratively describe the relationship between the UI and data. reactive
and ref
are the two primary ways to create reactive data in Vue 3. While both ultimately achieve reactive data tracking, they differ significantly in their underlying implementation and usage scenarios.
Basic Principles of Reactive
The reactive
function takes a plain object and returns a Proxy wrapper for that object. Proxy is an ES6 feature that allows intercepting fundamental operations on the target object. Vue 3 uses Proxy's get and set traps to implement dependency tracking and update triggering.
const obj = reactive({ count: 0 })
// Simplified Proxy implementation
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
track(target, key) // Dependency collection
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
trigger(target, key) // Trigger updates
return result
}
})
}
When accessing obj.count
, the Proxy's get trap executes, and Vue records the currently running effect (side-effect function). When modifying obj.count
, the set trap triggers, and Vue finds all effects that depend on this property and re-executes them.
Basic Principles of Ref
ref
is primarily used to wrap primitive values (e.g., numbers, strings) because Proxy cannot directly proxy primitives. Ref achieves this by creating a reactive object with a value
property:
const count = ref(0)
// Simplified implementation
function ref(value) {
const refObject = {
get value() {
track(refObject, 'value')
return value
},
set value(newVal) {
value = newVal
trigger(refObject, 'value')
}
}
return refObject
}
When using ref in templates, Vue automatically unwraps it, so .value
is not needed. However, in JavaScript, .value
must be used:
// In templates
<template>
<div>{{ count }}</div> <!-- Auto-unwrapping -->
</template>
// In JS
console.log(count.value) // Requires .value
Comparison of Differences
-
Data Type Handling:
reactive
only accepts object typesref
accepts any type, including primitives and objects
-
Access Method:
reactive
objects access properties directlyref
requires accessing via.value
-
Replacing Entire Objects:
reactive
loses reactivity if replaced directlyref
can be replaced entirely because reactivity is bound to the ref object itself
// Reactive limitation
const state = reactive({ count: 0 })
state = { count: 1 } // Loses reactivity
// Ref flexibility
const stateRef = ref({ count: 0 })
stateRef.value = { count: 1 } // Maintains reactivity
Workflow of the Reactivity System
-
Dependency Collection Phase: When a component renders, the render function executes. Accessing reactive data triggers the getter, registering the currently executing effect (render function) as a dependency.
-
Update Triggering Phase: When reactive data changes, the setter triggers, notifying all dependent effects to re-execute, thereby updating the UI.
// Simplified effect implementation
let activeEffect
function effect(fn) {
activeEffect = fn
fn() // Execution triggers getters, collecting dependencies
activeEffect = null
}
// Dependency collection
const depsMap = new WeakMap()
function track(target, key) {
if (!activeEffect) return
let dep = depsMap.get(target)
if (!dep) {
dep = new Map()
depsMap.set(target, dep)
}
let effects = dep.get(key)
if (!effects) {
effects = new Set()
dep.set(key, effects)
}
effects.add(activeEffect)
}
// Trigger updates
function trigger(target, key) {
const dep = depsMap.get(target)
if (!dep) return
const effects = dep.get(key)
effects && effects.forEach(effect => effect())
}
Practical Use Cases
- Reactive Use Cases:
- Complex object structures
- Scenarios requiring destructuring (with
toRefs
) - Nested data structures like form objects
const form = reactive({
user: {
name: '',
age: 0
},
preferences: {
theme: 'light'
}
})
// Destructuring while maintaining reactivity
const { user, preferences } = toRefs(form)
- Ref Use Cases:
- Primitive values
- Reactive variables needing reassignment
- Template refs for DOM elements
- Composition function return values
// DOM reference
const inputRef = ref(null)
// Composition function
function useCounter() {
const count = ref(0)
const increment = () => count.value++
return { count, increment }
}
Performance Considerations
-
Memory Overhead:
reactive
creates a Proxy for the entire objectref
creates a wrapper object for each value
-
Access Speed:
reactive
direct property access is slightly fasterref
requires an additional.value
access
-
Batch Updates: Vue processes updates asynchronously in batches. Regardless of the method used, multiple modifications will ultimately trigger only one re-render.
Advanced Reactive APIs
- shallowRef:
Only reacts to
.value
reference changes, without deep tracking of internal values.
const shallow = shallowRef({ count: 0 })
shallow.value.count++ // Won't trigger updates
shallow.value = { count: 1 } // Will trigger updates
- readonly: Creates a read-only reactive proxy. Any modification attempts will fail.
const ro = readonly({ count: 0 })
ro.count++ // Warning and failure
- customRef: Creates a custom ref, allowing control over dependency tracking and update triggering timing.
function debouncedRef(value, delay = 200) {
let timeout
return customRef((track, trigger) => {
return {
get() {
track()
return value
},
set(newValue) {
clearTimeout(timeout)
timeout = setTimeout(() => {
value = newValue
trigger()
}, delay)
}
}
})
}
Comparison with Vue 2
-
Limitations of Object.defineProperty:
- Cannot detect property addition/deletion
- Requires special handling for array mutation methods
- Needs recursive traversal of all object properties
-
Advantages of Proxy:
- Can intercept more operations (e.g.,
in
,delete
) - Better performance, reactive on demand
- Supports new collection types like Map and Set
- Can intercept more operations (e.g.,
// Vue 2's reactivity implementation example
function defineReactive(obj, key) {
let value = obj[key]
const dep = new Dep()
Object.defineProperty(obj, key, {
get() {
dep.depend() // Collect dependencies
return value
},
set(newVal) {
value = newVal
dep.notify() // Trigger updates
}
})
}
Common Issues and Solutions
- Reactivity Loss:
Destructuring a reactive object causes reactivity loss. Use
toRefs
instead.
const state = reactive({ count: 0 })
// Wrong way - loses reactivity
let { count } = state
// Correct way
let { count } = toRefs(state)
- Circular References: Reactive handles circular references automatically but beware of infinite recursion.
const obj = reactive({})
obj.self = obj // Allowed but not recommended
- Primitive Value Wrapping: Directly unwrapping a ref into a reactive object maintains reactivity linkage.
const count = ref(0)
const state = reactive({ count })
count.value++ // state.count also updates
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
上一篇:组件状态共享模式比较
下一篇:Proxy实现响应式