Refactoring the reactive system (replacing defineProperty with Proxy)
Reactive System Refactoring (Proxy Replacing defineProperty)
Vue.js 2.x uses Object.defineProperty
to implement its reactive system, while Vue 3 refactors the entire reactivity mechanism using Proxy
. This change brings performance improvements and enhanced functionality while also addressing some inherent limitations of defineProperty
.
Limitations of defineProperty
Object.defineProperty
has several key issues:
- Cannot detect property addition or deletion:
const obj = {}
Object.defineProperty(obj, 'a', {
get() { return this._a },
set(val) {
console.log('set a')
this._a = val
}
})
obj.a = 1 // triggers set
obj.b = 2 // no reactivity triggered
- Array mutation methods require special handling:
const arr = []
Object.defineProperty(arr, 'length', { /* ... */ })
arr.push(1) // does not trigger length setter
- Significant performance overhead, as it requires recursively traversing all object properties for conversion.
Advantages of Proxy
ES6's Proxy perfectly solves these issues:
const handler = {
get(target, key) {
console.log(`Get ${key}`)
return Reflect.get(target, key)
},
set(target, key, value) {
console.log(`Set ${key} = ${value}`)
return Reflect.set(target, key, value)
},
deleteProperty(target, key) {
console.log(`Delete ${key}`)
return Reflect.deleteProperty(target, key)
}
}
const proxy = new Proxy({}, handler)
proxy.a = 1 // outputs "Set a = 1"
proxy.a // outputs "Get a"
delete proxy.a // outputs "Delete a"
Vue 3's Reactive Implementation
Core implementation of Vue 3's reactive
:
function reactive(target) {
const handler = {
get(target, key, receiver) {
track(target, key) // dependency collection
const res = Reflect.get(target, key, receiver)
if (typeof res === 'object' && res !== null) {
return reactive(res) // deep reactivity
}
return res
},
set(target, key, value, receiver) {
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
if (oldValue !== value) {
trigger(target, key) // trigger updates
}
return result
}
}
return new Proxy(target, handler)
}
Performance Comparison
-
Initialization performance:
- defineProperty: Requires recursive traversal of all properties
- Proxy: Processes only on access (lazy conversion)
-
Memory usage:
- defineProperty: Requires creating closures for each property to store dependencies
- Proxy: Shares a single handler for the entire object
-
Array handling:
const arr = reactive([1, 2, 3])
arr.push(4) // triggers reactivity normally
arr.length = 10 // also triggers reactivity
Practical Use Cases
- Dynamic properties:
const state = reactive({})
// Can dynamically add reactive properties
state.newProp = 'value'
- Map/Set support:
const map = reactive(new Map())
map.set('key', 'value') // reactive update
- Better TypeScript support:
interface State {
count: number
user?: {
name: string
}
}
const state: State = reactive({
count: 0
})
// Type-safe access
state.count++
Compatibility Considerations
Although Proxy is a modern browser feature, Vue 3 provides a fallback solution:
function createReactive(target) {
return ProxySupported
? new Proxy(target, handler)
: fallbackDefineProperty(target)
}
For scenarios requiring IE support, the @vue/compat
compatibility build can be used.
Evolution of Reactive APIs
Vue 3 introduces a series of new reactive APIs:
import { reactive, ref, computed, watchEffect } from 'vue'
const count = ref(0)
const double = computed(() => count.value * 2)
const state = reactive({ list: [] })
watchEffect(() => {
console.log(`count is ${count.value}`)
})
Comparison with Vue 2
- Array detection:
// Vue 2
this.$set(this.arr, index, value)
// Vue 3
arr[index] = value // direct assignment works
- Nested objects:
// Vue 2
this.$set(this.obj, 'nested', {})
// Vue 3
obj.nested = {} // automatically reactive
Source Code Analysis
Simplified dependency collection implementation:
const targetMap = new WeakMap()
function track(target, key) {
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
dep.add(activeEffect)
}
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
effects && effects.forEach(effect => effect.run())
}
Best Practices
- Handling large objects:
// Avoid making large objects reactive at once
const bigData = shallowReactive({ /* large dataset */ })
- Performance-sensitive scenarios:
// Use markRaw to skip reactive conversion
import { markRaw } from 'vue'
const nonReactive = markRaw({ shouldNotTrack: true })
- Composable functions:
function useCounter() {
const count = ref(0)
const double = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, double, increment }
}
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn