Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Understanding Vue 3 Reactivity System Internals

Tech May 15 1

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

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.