Vue3 Reactive System: Dependency Tracking and Update Triggering Principles

Vue3 Reactive System: Dependency Tracking and Update Triggering Principles

Problem Description
Understand the core mechanisms of dependency tracking (track) and update triggering (trigger) in Vue3's reactive system, including how to establish associations between reactive data and side-effect functions, and how to precisely notify relevant dependencies when data changes.

Knowledge Explanation

1. Core Goals of the Reactive System

  • When reactive data changes, automatically execute side-effect functions that depend on that data (such as component render functions, computed properties, watch listeners, etc.)
  • Need to establish a mapping relationship between data properties and side-effect functions to achieve precise updates

2. Basic Concept Definitions

Side-Effect Function (Effect)

// Any function that reads reactive data is a side-effect function
const effect = () => {
  document.body.innerText = obj.text // Reading obj.text
}

Dependency Relationship (Deps Map)

  • Uses a three-level mapping structure:
    • TargetMap: WeakMap, where keys are reactive objects and values are DepsMaps
    • DepsMap: Map, where keys are object property names and values are Dep Sets
    • Dep: Set, storing all side-effect functions that depend on this property

3. Dependency Tracking (Track) Process

Step 1: Establish Data Structure

const targetMap = new WeakMap() // Global dependency storage

function track(target, key) {
  if (!activeEffect) return // Do not track if no active side-effect function
  
  // Level 1: Find or create the depsMap corresponding to the target
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }
  
  // Level 2: Find or create the dep set corresponding to the key
  let dep = depsMap.get(key)
  if (!dep) {
    dep = new Set()
    depsMap.set(key, dep)
  }
  
  // Level 3: Add the current active side-effect function to the dep
  trackEffects(dep)
}

function trackEffects(dep) {
  if (dep.has(activeEffect)) return // Avoid duplicate collection
  
  dep.add(activeEffect)
  // Simultaneously, the side-effect function also needs to record which deps it belongs to (for cleanup)
  activeEffect.deps.push(dep)
}

Step 2: Proxy Getter Interception

function createGetter() {
  return function get(target, key, receiver) {
    const res = Reflect.get(target, key, receiver)
    
    // Dependency tracking
    track(target, key)
    
    if (isObject(res)) {
      return reactive(res) // Deep reactivity
    }
    
    return res
  }
}

4. Update Triggering (Trigger) Process

Step 1: Find Dependencies and Execute

function trigger(target, key) {
  // Find the depsMap corresponding to the target from targetMap
  const depsMap = targetMap.get(target)
  if (!depsMap) return // Return directly if no dependencies
  
  // Find the dep set corresponding to the key
  const dep = depsMap.get(key)
  if (dep) {
    triggerEffects(dep)
  }
}

function triggerEffects(dep) {
  // Avoid infinite loops caused by triggering collection while executing effects
  const effects = new Set(dep)
  
  effects.forEach(effect => {
    // If the effect has a scheduler, execute via the scheduler (for optimizations like batch updates)
    if (effect.scheduler) {
      effect.scheduler()
    } else {
      effect() // Directly execute the side-effect function
    }
  })
}

Step 2: Proxy Setter Interception

function createSetter() {
  return function set(target, key, value, receiver) {
    const oldValue = target[key]
    const result = Reflect.set(target, key, value, receiver)
    
    // Trigger update (only trigger when the value actually changes)
    if (hasChanged(value, oldValue)) {
      trigger(target, key)
    }
    
    return result
  }
}

5. Complete Reactive Function Example

let activeEffect = null

class ReactiveEffect {
  constructor(fn, scheduler) {
    this.fn = fn
    this.scheduler = scheduler
    this.deps = [] // Record which deps have collected this effect
  }
  
  run() {
    activeEffect = this
    const result = this.fn()
    activeEffect = null
    return result
  }
}

function effect(fn, options = {}) {
  const _effect = new ReactiveEffect(fn, options.scheduler)
  _effect.run()
  
  // Return a runner function that can be manually executed
  const runner = _effect.run.bind(_effect)
  runner.effect = _effect
  return runner
}

6. Dependency Cleanup Mechanism

Why Cleanup is Needed:

  • Side-effect functions may no longer depend on certain data and need to be removed from the corresponding dep
  • Avoid memory leaks and unnecessary updates

Cleanup Implementation:

class ReactiveEffect {
  constructor(fn, scheduler) {
    // ...other code
    this.deps = []
  }
  
  run() {
    // First, clean up previous dependencies
    cleanupEffect(this)
    activeEffect = this
    const result = this.fn()
    activeEffect = null
    return result
  }
}

function cleanupEffect(effect) {
  const { deps } = effect
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      deps[i].delete(effect) // Remove this effect from all deps
    }
    deps.length = 0 // Clear the deps array
  }
}

7. Practical Application Example

const obj = reactive({ count: 0, name: 'vue' })

// Side-effect function 1: Only depends on count
effect(() => {
  console.log('count changed:', obj.count)
})

// Side-effect function 2: Depends on both count and name
effect(() => {
  console.log('both changed:', obj.count, obj.name)
})

obj.count++ // Triggers both effects
obj.name = 'vue3' // Only triggers the second effect

Summary
Vue3's dependency tracking and update triggering mechanism establishes precise dependency relationships through a three-level mapping structure. It collects dependencies during get operations and triggers updates during set operations. By tracking the currently active side-effect function via effect, combined with the data structures WeakMap, Map, and Set, it achieves efficient reactive updates. The cleanup mechanism ensures the accuracy of dependency relationships and avoids memory leaks.