Understanding Vue 3 Reactivity System Internals
Proxy-Based Reactive Data Handling
The reactive system can be implemented using JavaScript's Proxy API to intercept property access and modifications:
const userData = {
username: 'alexchen',
userAge: 25
}
function createReactive(source) {
const interceptor = {
get(source, property, receiver) {
console.log(`Reading property: ${property}`)
return Reflect.get(source, property, receiver)
},
set(source, property, newValue, receiver) {
console.log(`Updating ${property} from ${source[property]} to ${newValue}`)
Reflect.set(source, property, newValue, receiver)
return true
}
}
return new Proxy(source, interceptor)
}
const reactiveData = createReactive(userData)
console.log(reactiveData.username)
// Reading property: username
// alexchen
reactiveData.username = 'alex_dev'
// Updating username from alexchen to alex_dev
console.log(reactiveData.username)
// Reading property: username
// alex_dev
Unlike Object.defineProperty, Proxy can detect newly added properties:
reactiveData.userRole = 'developer'
console.log(reactiveData.userRole)
// Reading property: userRole
// developer
reactiveData.userRole = 'senior_developer'
// Updating userRole from developer to senior_developer
console.log(reactiveData.userRole)
// Reading property: userRole
// senior_developer
Dependency Tracking Fundamentals
Consider this scenario where a computed value doesn't automatically update:
let developerName = 'alex', developerAge = 25, developerSalary = 50000
let profileSummary = `${developerName} is ${developerAge} years old with $${developerSalary} salary`
console.log(profileSummary) // alex is 25 years old with $50000 salary
developerSalary = 75000
console.log(profileSummary) // alex is 25 years old with $50000 salary (unchanged)
To make profileSummary reactive, we need to re-execute the computation:
let developerName = 'alex', developerAge = 25, developerSalary = 50000
let profileSummary = ''
function updateProfile() {
profileSummary = `${developerName} is ${developerAge} years old with $${developerSalary} salary`
}
updateProfile()
console.log(profileSummary) // alex is 25 years old with $50000 salary
developerSalary = 75000
updateProfile()
console.log(profileSummary) // alex is 25 years old with $75000 salary
Effect Functions and Dependency Collection
For multiple dependent values, we can use effect functions:
let developerName = 'alex', developerAge = 25, developerSalary = 50000
let profileSummary = '', alternateSummary = ''
const updateProfile = () => profileSummary = `${developerName} is ${developerAge} years old with $${developerSalary} salary`
const updateAlternate = () => alternateSummary = `${developerAge}-year-old ${developerName} earns $${developerSalary}`
updateProfile()
updateAlternate()
console.log(profileSummary) // alex is 25 years old with $50000 salary
console.log(alternateSummary) // 25-year-old alex earns $50000
developerSalary = 75000
updateProfile()
updateAlternate()
console.log(profileSummary) // alex is 25 years old with $75000 salary
console.log(alternateSummary) // 25-year-old alex earns $75000
Track and Trigger Mechanisms
To automate dependency collection, we use track and trigger functions:
let developerName = 'alex', developerAge = 25, developerSalary = 50000
let profileSummary = '', alternateSummary = ''
const updateProfile = () => profileSummary = `${developerName} is ${developerAge} years old with $${developerSalary} salary`
const updateAlternate = () => alternateSummary = `${developerAge}-year-old ${developerName} earns $${developerSalary}`
const dependencies = new Set()
function collectDependencies() {
dependencies.add(updateProfile)
dependencies.add(updateAlternate)
}
function notifyUpdates() {
dependencies.forEach(effectFn => effectFn())
}
collectDependencies()
updateProfile()
updateAlternate()
console.log(profileSummary)
console.log(alternateSummary)
developerSalary = 75000
notifyUpdates()
console.log(profileSummary)
console.log(alternateSummary)
Object-Level Reactivity
For object properties, each property maintains its own dependency set:
const employee = { empName: 'alex', empAge: 25 }
let nameDisplay1 = ''
let nameDisplay2 = ''
let ageDisplay1 = ''
let ageDisplay2 = ''
const updateName1 = () => { nameDisplay1 = `${employee.empName} is a junior developer` }
const updateName2 = () => { nameDisplay2 = `${employee.empName} is a senior developer` }
const updateAge1 = () => { ageDisplay1 = `${employee.empAge} is relatively young` }
const updateAge2 = () => { ageDisplay2 = `${employee.empAge} is approaching mid-career` }
const propertyMap = new Map()
function trackProperty(key) {
let depSet = propertyMap.get(key)
if (!depSet) {
propertyMap.set(key, depSet = new Set())
}
if (key === 'empName') {
depSet.add(updateName1)
depSet.add(updateName2)
} else {
depSet.add(updateAge1)
depSet.add(updateAge2)
}
}
function triggerProperty(key) {
const depSet = propertyMap.get(key)
if (depSet) {
depSet.forEach(effectFn => effectFn())
}
}
trackProperty('empName')
trackProperty('empAge')
updateName1()
updateName2()
updateAge1()
updateAge2()
console.log(nameDisplay1, nameDisplay2, ageDisplay1, ageDisplay2)
employee.empName = 'alex_chen'
employee.empAge = 30
triggerProperty('empName')
triggerProperty('empAge')
console.log(nameDisplay1, nameDisplay2, ageDisplay1, ageDisplay2)
Multi-Object Dependency Management
Using WeakMap for managing multiple reactive objects:
const developer = { devName: 'alex', devAge: 25 }
const project = { projectName: 'vue_app', projectHours: 40 }
const objectRegistry = new WeakMap()
function trackObjectProperty(target, key) {
let propertyMap = objectRegistry.get(target)
if (!propertyMap) {
objectRegistry.set(target, propertyMap = new Map())
}
let depSet = propertyMap.get(key)
if (!depSet) {
propertyMap.set(key, depSet = new Set())
}
if (target === developer) {
if (key === 'devName') {
depSet.add(updateDevName1)
depSet.add(updateDevName2)
} else {
depSet.add(updateDevAge1)
depSet.add(updateDevAge2)
}
} else if (target === project) {
if (key === 'projectName') {
depSet.add(updateProjectName1)
depSet.add(updateProjectName2)
} else {
depSet.add(updateProjectHours1)
depSet.add(updateProjectHours2)
}
}
}
function triggerObjectProperty(target, key) {
let propertyMap = objectRegistry.get(target)
if (propertyMap) {
const depSet = propertyMap.get(key)
if (depSet) {
depSet.forEach(effectFn => effectFn())
}
}
}
Automated Dependency Tracking
Combining Proxy with automatic dependency collection:
function makeReactive(target) {
const handler = {
get(target, key, receiver) {
trackObjectProperty(receiver, key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
Reflect.set(target, key, value, receiver)
triggerObjectProperty(receiver, key)
}
}
return new Proxy(target, handler)
}
Dynamic Effect Registration
Eliminating hardcoded dependency registration:
let currentEffect = null
function executeEffect(fn) {
currentEffect = fn
currentEffect()
currentEffect = null
}
function trackObjectProperty(target, key) {
if (!currentEffect) return
let propertyMap = objectRegistry.get(target)
if (!propertyMap) {
objectRegistry.set(target, propertyMap = new Map())
}
let depSet = propertyMap.get(key)
if (!depSet) {
propertyMap.set(key, depSet = new Set())
}
depSet.add(currentEffect)
}
executeEffect(updateDevName1)
executeEffect(updateDevName2)
executeEffect(updateDevAge1)
executeEffect(updateDevAge2)
Ref Implementation
Creating reactive references:
function createRef(initialValue) {
return makeReactive({
value: initialValue
})
}
let counter = createRef(10)
executeEffect(() => total = counter.value * 50)
console.log(total) // 500
counter.value = 20
console.log(total) // 1000
Computed Properties
Implementing computed values:
function createComputed(computationFn) {
const computedRef = createRef()
executeEffect(() => computedRef.value = computationFn())
return computedRef
}
let firstNumber = createRef(8)
let secondNumber = createRef(12)
let productResult = createComputed(() => firstNumber.value * secondNumber.value)
let scaledResult = createComputed(() => productResult.value * 5)
console.log(productResult.value) // 96
console.log(scaledResult.value) // 480
firstNumber.value = 15
console.log(productResult.value) // 180
console.log(scaledResult.value) // 900
Complete Implementation
const globalRegistry = new WeakMap()
function trackDependency(target, key) {
if (!currentEffect) return
let propertyMap = globalRegistry.get(target)
if (!propertyMap) {
globalRegistry.set(target, propertyMap = new Map())
}
let depSet = propertyMap.get(key)
if (!depSet) {
propertyMap.set(key, depSet = new Set())
}
depSet.add(currentEffect)
}
function triggerDependency(target, key) {
let propertyMap = globalRegistry.get(target)
if (propertyMap) {
const depSet = propertyMap.get(key)
if (depSet) {
depSet.forEach(effectFn => effectFn())
}
}
}
function makeReactive(target) {
const handler = {
get(target, key, receiver) {
trackDependency(receiver, key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
Reflect.set(target, key, value, receiver)
triggerDependency(receiver, key)
}
}
return new Proxy(target, handler)
}
let currentEffect = null
function executeEffect(fn) {
currentEffect = fn
currentEffect()
currentEffect = null
}
function createRef(initialValue) {
return makeReactive({
value: initialValue
})
}
function createComputed(computationFn) {
const resultRef = createRef()
executeEffect(() => resultRef.value = computationFn())
return resultRef
}
Proxy and Reflect Integration
Proxy intercepts object operations while Reflect provides corresponding methods:
const teamMember = { memberName: 'alex', memberAge: 25 }
const reactiveMember = new Proxy(teamMember, {
get(target, key, receiver) {
console.log(target) // Original object
console.log(key) // Property name
console.log(receiver) // Proxy instance
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
console.log(target) // Original object
console.log(key) // Property name
console.log(value) // New value
console.log(receiver) // Proxy instance
return Reflect.set(target, key, value, receiver)
}
})
reactiveMember.memberName
reactiveMember.memberName = 'alex_dev'
Using Reflect ensures proper context handling and prevents infinite recursion:
const teamMember = { memberName: 'alex', memberAge: 25 }
const reactiveMember = new Proxy(teamMember, {
get(target, key, receiver) {
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
return Reflect.set(target, key, value, receiver)
}
})
console.log(reactiveMember.memberName) // alex
reactiveMember.memberName = 'alex_dev'
console.log(reactiveMember.memberName) // alex_dev